I recently came across this tweet asking for thoughts on how people model screens as state in Android. I started writing a reply but I just couldn’t fit it into a bite-sized response.
From my experience, all three patterns proposed could be used for simple screens that are comprised of a single item. However, screens almost inevitably gain complexity and wind up with more complex state over time. In the following section, I’ll highlight the challenges with each one when introducing a second set of data and then I’ll propose the solution that has been working well for my team for the past four years (hint: it’s mavericks).
When you add a second item, you have to rename the loading and error states. Consumers then have to check multiple properties to know the true loading/error state.
Top-level sealed classes are even harder to scale. If you add a second data source that can independently load and fail, you wind up with a combinatorial explosion of different states. Representing the entire screen as Success/Loading holistically quickly no longer makes sense.
Having multiple streams of data adds a lot of boilerplate complexity and overhead. Combining multiple properties requires using Flow/LiveData operators such as combine and you quickly get a tangled mess of individual subscriptions. This model is prone to the event-based challenges of an event bus because each subscription can modify any view and your final view state winds up being affected by the order in which subscriptions were made which is inherently brittle and error-prone.
While at Airbnb (and now Tonal), all of these patterns were being explored within the codebase and each one encountered the pitfalls above. That is why we created Mavericks. Mavericks is designed such that your ViewModel is generic on a single state class and your View/Fragment should be a pure function of state.
With Mavericks, the state would look like this:
Notice that the items are of type Async<List<Item>>. Async is a sealed class that neatly encapsulates the loading, error, and success state and their respective values. It can even store previously successful values for handling things like pull to refresh.
With Mavericks, the complexity of a screen stays relatively constant as the number of items grows. Adding a second pair of items looks like this:
Notice how we can create new properties inside of the body of our state class. In the Mavericks world, we call these derived properties. Derived properties are incredibly useful because they are guaranteed to be up to date because they are calculated as a direct result of the immutable data class properties.
Using it is fairly simple too:
There is no additional wiring or boilerplate required to make this work and the complexity of this screen should grow linearly, not exponentially with the amount of data it encapsulates.
Side note on LiveData: now that StateFlow is stable and Jetpack has powerful lifecycle extensions for it, I would fully endorse coroutines + Flow/StateFlow over LiveData or RxJava for all new applications. Don’t take my word for it. Yigit Boyar and Manuel Vicente Vivo outline that here and here.
I hope this was helpful! Let me know what you think, if you disagree, or if you have another way of handling this in the comments or on my Twitter.