Android Architectural components in Box App

Box
Box Tech Blog
Published in
6 min readDec 19, 2018

by Don Cung, Jimmy Holzer and Nyjal Augustine

Overview

On May 17, 2017 Google announced Architecture Components that created lot of buzz in the Android community. It was the first time Google laid out an opinionated blueprint for building scalable apps. At Box, we were keeping an eye on the development of the library and come 2018 we felt it was time to dive-in with our project of refactoring Preview SDK. Developing robust Android apps can be challenging because one needs to handle complex lifecycle issues, unreliable mobile networks, and constrained device capabilities. Mistakes in these areas lead to memory leaks, crashing apps, drained batteries, and unhappy users.

Architecture components offers the capability to implement Clean Architecture easily. On a high level, there are three main layers:

  • Views/UI: This is the UI layer consisting of Android Activity, Fragment and other views. Users directly interact with this layer.
  • ViewModel: Provides data to the UI and acts as a communication layer between the Repository and the UI. It doesn’t hold references to UI components, so it isn’t affected by configuration changes, such as recreating an activity when device rotates.
  • Repository: Layer for managing multiple data sources such as web services, local cache etc. Provides a clean interface to fulfill the needs of ViewModel.

LiveData

The data flows between these layers in a special observable data holder object called LiveData. The best part is that LiveData is a lifecycle aware object so we get subscription management for free meaning it can automatically unsubscribe itself when the observer is dead. This makes using the Observer design pattern very convenient.

Use case

Preview experience for image and video
Audio playback experience

Before we dive into the technicalities, it is important to understand our app’s use case. When a user selects a file to preview we first filter all the items from the folder based on their type and then display the selected item. Filtering is done to group all the Images, GIF’s and Videos together whereas during audio playback all the audio files will be grouped together. Swiping in this case will load other filtered items. For a better user experience we also preload our fragments.

Repository setup

Data flow across different layers

Our ViewModels behave quite normally acting as middlemen between the views and the specific content repos. They will expose the ContentRepo’s livedata to the views to track changes such as the song changing as well as call methods on the repo for instance if the user selects a new song to play. Our content repositories are most notable in that they are specific to a given user, and act as the source of truth for what that user wants to preview.

Initialization flow of different components

We delegate the job of instantiating these user specific content repos to our activities which are launched with an intent containing information such as what file to play and the authentication information the content is tied to. Using this information the activity decides whether it can reuse existing repositories or if it needs to instantiate a new set. If a new set is instantiated the livedata in the singleton is updated making this content repo the most current. The activity can then create a ViewModel Factory with this set of repositories which allows for the expected view model interaction above.

Views & ViewModel setup

As highlighted in the above diagram we aimed to keep good layer of separation. Your UI layer (Activity, Fragment, …) is only allowed to access the View Model layer. Creating a new ViewModel Object without constructor is quite easy. From a Fragment or Activity you can just call:

The ViewModelProvider will create OR return existing View Model for given scope (of(this)). It means that subsequent calls with the same scope will return the same ViewModel instance.

However chances are that your View Model need parameters, in this case, you will need to use a factory. All our ViewModel take a repository as parameter in their constructor. Here is our setup:

With the following Factory class:

The challenge is to setup the factory with the Repository. We have encapsulated all our repositories inside a RepoContainer class that is exposed through a LiveData inside a PreviewRepo singleton.

We decided to expose our repositories as liveData because our SDKs support multi-users. The onCreateView of our Fragment will get their view model like this:

However this choice forces us to initialize the Factory and access the RepoContainer from the UI layer which breaks our layer of separation. This is clearly a point we are looking to improve.

LiveData Transformation

It is possible that once we have filtered the items something changes under the hood for eg. one of the file in the filtered list gets deleted from the Box server. Such an adhoc change should be supported by the app. We do this by creating a new SelectedItemContainer LiveData by merging two other LiveData’s. One is a LiveData of List of filtered BoxFile’s and the other is a LiveData of currently visible BoxFile position in the list. Any changes in the source LiveData triggers a change in the SelectedItemContainer LiveData which immediately updates the UI.

Multi-user challenges

One of our unique use cases is that our users might have more than one Box account, and so we need to be able to handle events such as user’s switching between accounts to preview different content. Since our SDK is used by third parties it is also difficult to force a restricted user flow.

As explained in the repository setup section much of our solution is based on having the concept of the most current set of repositories. When our activities instantiate a new set of repositories they update a singleton livedata that can be observed by long lived components namely a service that plays can play a list of songs. This service has a one to many relationship with repositories in the sense that it needs to know if any repository moves to an audio file, it should start playing that list but if otherwise it should continue playing the songs for its last repo. By monitoring the current repository livedata the service can observe all new repos, and if the latest repo starts playing a song it can release its observers on older repos.

Finally we use this singleton livedata to handle changes from outside of our SDK. By exposing a reset method on our singleton our service which monitors the livedata can stop playback and cleanup data in response to a user action, in our case if the user switches accounts we want to stop audio playback.

References:

Interested in joining Box? Check out our open positions.

--

--

Box
Box Tech Blog

A new way to work. ⁣Questions about your account? @BoxSupport has your back!