Managing Consistency on Android

Priyesh Patel
500px Engineering Blog
6 min readDec 16, 2016

--

Users of apps expect that their interactions are reflected throughout all areas of the interface. For example, after a user deletes their photo, it should no longer be visible in other areas such as their profile or feed. Achieving this can be problematic for views retained in memory, where the data the views present can become stale or outdated. Consistency is something users take for granted. If you don’t have it, it will be noticeable and will reduce the confidence users have in your app. In this post, we’ll discuss our approach to managing consistency on Android.

To go back to the example above, when a user deletes one of their photos, we want to remove it from the feed and their profile. FeedFragment and ProfileFragment are retained in memory while the app is running using a standard ViewPager embedded within the single instance of our main Activity. The important thing to note here is that these fragments are instantiated only once for the duration of the app’s lifetime. As a result, the underlying data that they each present can be become stale in the event that the data is modified elsewhere. In order to keep the fragments up to date with the newest data models available, some further action is required. There are a few approaches to achieving this:

  • Define interfaces, allowing views to communicate with each other
  • Global notification mechanism (centralized event bus)
  • Publish-subscribe system

The first solution that may come to mind is to simply use an interface to allow FeedFragment and ProfileFragment to listen for changes made in PhotoFragment. In this solution, PhotoFragment holds a reference to each of the listeners and notifies them of the deletion. Each listening fragment is then responsible for removing the item from its internal state and updating its interface accordingly. However, there are a few issues with this approach. For one, PhotoFragment now has responsibility beyond its initial scope of simply presenting a photo. It should not be concerned with coordinating updates in surrounding fragments, let alone know of their existence. Another drawback to this solution is the lack of elegant scaling. Whenever a new section of the app needs to respond to photo deletions, we must add more extraneous code to PhotoFragment. For common actions in a large app, this can quickly lead to tightly coupled code where there is no clear separation of concerns.

Another solution is to use a global notification mechanism, or an event bus. In this approach, changes are represented as event objects and are broadcasted to a receiver. The receiver then determines the event type, dissects any necessary information, and handles it accordingly. This was the route taken previously on our iOS app, using NSNotificationCenter. Although this loosens the coupling between views, we found it had some drawbacks. The number of event types quickly became difficult to manage, especially when trying to relay specific information about each event such as failure, success, and completion. Another annoyance we encountered with NSNotificationCenter was that notifications (events) are identified by strings. This led to the app being polluted with string constants, in an attempt to workaround the loss of type safety.

The last approach we’ll discuss is the publish-subscribe model. The idea here is that publishers send messages without knowing specifically where they will be received. Subscribers then subscribe to the types of messages that they’re interested in receiving. This system allows data producers to be almost entirely decoupled from data consumers. Scalability is another benefit of this model. Adding support for additional data types simply requires writing new publishers and subscribers, and doesn’t interfere with the existing ones. For these reasons we decided to use the pub-sub model at the core of our solution.

Enter Jackie

Jackie is a consistency cache for Android that we built at 500px. At a high level, Observers subscribe to DataItems (model objects) and are notified of changes. Jackie maintains a centralized cache of your data and lets you easily circulate updates of the data throughout your application. DataItem is a simple interface that requires classes to implement the getId() method as a unique identifier of the item’s instance. Unique identifiers let us pass around immutable copies of objects, rather than modify a single object instance, so that we don’t have to worry about multiple threads mutating the same object. In order to conveniently update an object in an immutable manner, we make use of Lombok’s Wither annotation. This lets us clone an object while changing a single field. This can be thought of as an immutable setter. Here’s a basic example to illustrate this:

// A class implementing DataItem
User user = new User("Bob", 25);
ItemObserver<User> observer = nextUser -> {
// Updates to user will be received here
};
Jackie.chan().subscribe(observer).to(user);User updatedUser = user.withAge(26);Jackie.chan().update(updatedUser);

Now let’s have a look at how we would handle the photo deletion example discussed above using Jackie:

ProfileFragment & FeedFragmentListObserver<Photo> observer = removedPhotos -> {
mPhotosAdapter.unbind(removedPhotos);
};
List<Photo> photos = PhotoProvider.fetchPhotos();Jackie.chan().subscribe(observer).to("photos", photos);

First we define observers in each of the fragments where we want to listen for changes to photos. ListObserver provides a few other callbacks for more granular updates, but for the sake of simplicity, we’ve only implemented onItemsRemoved(). Inside of this method, we can update our fragment to reflect the removal of the given photos. Typically this would be notifying a list adapter of the changes; however, the implementation is entirely up to you. After the observers have been set up, all that’s left is to subscribe them to an identifier with some initially bound items.

There are a few things to note here. In order for all of this to work, Photo must have implemented theDataItem interface. As do most resources coming from a data store, Photos on 500px each have a unique identifier, so implementing getId() is as simple as returning it. Another thing to remember is to unsubscribe observers when necessary. In the realm of Android, this should be done when the fragment or activity is destroyed, to avoid leaking the observer:

@Override public void onDestroy() {
super.onDestroy()
Jackie.chan().unsubscribe(observer).from(photos);
}

Finally, in PhotoFragment we simply inform Jackie of the photo deletion when it occurs. Jackie takes care of notifying all subscribed observers.

PhotoFragmentvoid deletePhoto(Photo photo) {
Jackie.chan().remove(photo);
...
}

The beauty of this solution is the fact that PhotoFragment does not need to manually update other areas of the application. Leveraging Jackie to manage data consistency allows for concise and decoupled UI code and eliminates the need for communication between unrelated pieces of the interface.

Binding Endpoints to Views

We noticed that most of our fragments and activities had essentially the same behaviour. They all fetched data from an API, bound the data to views, and sometimes handled pagination. These tasks, while not very difficult, required lots of boilerplate code which was duplicated across many areas of our app. We took this as an opportunity to build an abstraction we call RestBinder to reduce the amount of setup and glue code needed in our views.

RestBinder lets us bind fragments and activities to an API endpoint we’re interested in accessing and uses Jackie under the hood to cache the incoming data. It also handles common use cases, such as pagination and refreshing, and accounts for the Android lifecycle by providing methods to easily subscribe and unsubscribe listeners, as well as save and restore state during configuration changes. From the listeners defined in our fragments and activities, we delegate presentation of the received data models to custom views. This is done via a method exposed on our views, <A> void bind(A a), where A is the data type which the view displays. This pattern lets us tuck away the often uninteresting presentation code and provides an explicit way of interacting with the view from the outside.

With these abstractions in place, our fragments and activities become extremely lean and their intentions are made clear. The responsibility of networking and presenting data is neatly contained, while the fragment or activity simply serves as a thin layer to handle delegation.

The Future of Jackie

Currently Jackie is used internally by the 500px Android team, allowing us to easily tackle consistency issues and make our code easier to maintain. Plans are in the works to polish the API and add features including RxJava bindings. We hope to eventually open source the project so that other Android developers can benefit from the power of Jackie!

--

--