zsmb.coAd-free by default



Thoughts about State Handling on Android

2020.06.04. 14h • Márton Braun

One of the frequent challenges on Android is keeping the UI of the application in a consistent, sensible state. We see failures of this every day: applications get stuck with weird, glitched out UI. These are the kind of issues that “should never happen”.

The root of the problem is state. The widget tree we work with is inherently stateful, quite literally full of state. Every node of the tree has dozens of properties that can mutate over time.

The stateful widget tree

Whenever Jetpack Compose comes along, the hope is that it will eliminate this issue entirely. But for now, we’re stuck having to wrangle this very mutable tree. Let’s see how we can make the best of this.

This article is part of a series of articles. This one is purely a theoretical introduction for state handling, but this will be put in practice in upcoming articles. Event handling will also be covered later on.

Disclaimer: these are all just subjective thoughts on the topic.

The classic MVP approach

Model-View-Presenter is perhaps the first well-known view architecture on Android that’s worth talking about. It’s been popular through many years of modern Android development, and it works alright most of the time. Most importantly, it does a good job of decoupling logic from the View (and the Android framework), greatly improving testability.

With this approach, we have a Presenter that coordinates what happens to the UI. The View sends input events to the Presenter, the Presenter reaches down into the Model layer to perform some operations or fetch some data (hopefully an exclusive or), and then calls methods on the View (abstracted away behind an interface) to update the state of the UI.

MVP

In my experience, this pattern is also notorious for causing the kind of bugs described in the introduction. The state of the UI here is essentially stored in the widget hierarchy managed by the View, and the Presenter continuously makes assumptions about the current state of the View while performing its work - without ever checking it, since it’s not supposed to know about the implementation of the View, or pull data out of it.

The Presenter then issues incremental updates to the View based on its assumptions, easily leading to problems such as two widgets being shown at the same time which should be mutually exclusive.

See this video for a classic example of this.

A lot of the time, the Presenter in MVP is stateless. However, there are implementations of MVP where the Presenter is stateful, and it performs checks on its current state while performing logic. The issue is that there is nothing really preventing this state in the Presenter from becoming out-of-sync with the state implicitly stored in the View, leading to invalid assumptions and bugs yet again.

Moving on to MVVM

Model-View-ViewModel can be thought of as an evolution of the MVP pattern. The ViewModel plays a similar role in this architecture as the Presenter in MVP - it sits between the View layer and the rest of the application, and coordinates interactions - but there are some key differences.

The ViewModel is always stateful by definition: it holds some kind of ViewState. Unlike the Presenter, it doesn’t communicate directly with the View. Instead, it exposes its current ViewState in some way, and the View has to subscribe to changes in this state, and update itself accordingly.

MVVM

The ViewState stored in the ViewModel is the single source of truth for UI state. While the widget hierarchy still technically stores its own state (unavoidably), it’s now the explicit responsibility of the View implementation to keep that in sync with the ViewState it observes from the ViewModel.

This can be covered with tests: all you need to do is start up a View, let it observe a given ViewState, and then assert the state of the UI. Plus, once you know that this binding is implemented well, you can test the logic in the ViewModel in isolation, and only make assertions on its ViewState in the tests - since you know that a given ViewState will be correctly reflected by the UI.

This strict one-to-one relation between ViewState and UI state is the core idea of MVVM.

Flavours of MVVM

MVVM can be implemented in many ways:

  • It can be used independently of any frameworks or libraries, with just pure classes.
  • You can also choose to use data binding to create a tight link between your ViewState and View - this makes keeping them in sync easier.
  • The ViewState can also be exposed by Rx constructs (a BehaviorSubject, most of the time), which are then subscribed to in the View.
  • There is an approach by Google, via their ViewModel and LiveData classes, provided as part of Architecture Components, first introduced in 2017.

ViewModel solves the issue of keeping your ViewModel instance during configuration changes (so that you don’t need to reload the ViewState), while LiveData makes the ViewState observable in a lifecycle-safe way. Notably, these can also be combined with data binding.

My personal preference is using ViewModel and LiveData, but not data binding. This feels like a healthy middle ground, where I get a lot of benefits from using these classes (lifecycle management), but I’m not too restricted in how I bind the ViewState to the UI in the View implementation. This last point also makes this approach friendly to beginners, who are used to manipulating the UI by getting View references and calling methods on them.

Single vs multiple view state(s?)

Many of the official Google samples use a lot of different LiveData instances to store the ViewState in. They then observe each LiveData separately in the View layer (a Fragment, most of the time).

To be fair, they do this mostly to use these values directly with data binding, which makes sense, as separate LiveData properties can be bound to View properties there, and updated individually. However, they also tend to stick with it without data binding.

This presents a risk of getting out-of-sync again. If you have two LiveData instances both storing parts of the ViewState, and there are dependencies between their values, then those rules can technically be broken.

For example, you can have a showLoading and a showError value to observe, and an implicit business rule that only one of these should be true at a time. However, there is technically nothing to prevent you from ending up with a screen that shows a progress indicator with an error message on top of it. It’s up to you to constantly pay attention to state handling in your code, and make sure you don’t set incorrect values to one of the LiveDatas, in relation to the other.

showLoading.value = true
// ...
showError.value = true // should have also set loading to false

Model-View-Intent, first introduced to the Android world perhaps by Hannes Dorfmann in a series of blog posts, gets even stricter about state. The first article in this series talks about state, and is well worth reading for more context. I won’t repeat every single idea laid out there in this article, but they’re worth being familiar with.

The core idea I want to take from MVI here is the idea of a single view state, that describes the state of the entire screen at a given time, in one object. This comes with a huge upside: you can design your ViewState model in a way that it doesn’t allow invalid states.

Read more about designing single view states in this previous post, including details of how Kotlin features such as sealed classes can aid you in your implementation.

Having just a single object describe what a screen should show makes reasoning about its code and current state easier. It also makes testing convenient, as already mentioned previously: tests for the View implementation consist of passing a ViewState instance to the View, and then asserting that it’s displayed correctly.

This doesn’t come for free, of course, so let’s mention some drawbacks that are worth being aware of:

  • If your state was split up into multiple observable pieces previously, you had the ability to update pieces of the UI separately. When you observed the change of a single Boolean or String value at a time, you could update the visibility of a single View or the content of a single TextView, without touching any other widgets of the screen. With a single ViewState, you have to render the entire screen on every change (or do some kind of diffing against the previous state, if you want to avoid this).
  • Given a complex enough screen, modelling and then creating instances of a single ViewState object that describes everything on it can be challenging, as it will lead to deeply nested immutable structures. Modifying this object, if it’s immutable (which it should be), might also be troublesome. The copy method of data classes or Arrow Optics can come to the rescue here to some degree.

While the single ViewState is by far my favourite part of MVI, it’s worth mentioning some of its other main ideas. It’s fully reactive, with Rx (or alternative reactive stream implementations) powering it top-to-bottom, from the input events down to a reducer and back to the View which gets updated with the new ViewState. This also necessitates input events to be converted into objects, so that they can be sent into Rx streams.

Personal opinion, again: I’m not the #1 fan of Rx, and since I’ll be using coroutines in the codebase anyway, I prefer to not have Rx involved as well. I see a certain appeal in wrapping input events in objects just like ViewState, and can see a lot of possibility in the idea - but generally speaking, I think most apps can do without this, just making regular method calls from the View to the ViewModel, and passing down parameters.

Persistent vs ephemeral state

How could we talk about the state of the View layer on Android without talking about process death? On a related note, we talked a lot about ViewState, but we haven’t taken a look at what’s in the ViewState, and how it gets there. Let’s cover these topics together.

I like to call the ViewState that’s stored in the ViewModel the persistent state of the application. This is a representation of state that’s already stored in some way. The ViewModel might populate this from a local database, network calls, or other similar data sources.

This state can be fetched at any time for a screen, based on the parameters that it was opened with. For example, in the case of a detail screen, an ID would be passed to it, which the ViewModel would use to load this persistent state into its ViewState, so that the View can display it.

With that, we have ViewState loaded into the ViewModel, and using the Jetpack ViewModel class for the implementation, the state will persist through configuration changes. While the state can be fetched at any time, this solution keeps it in memory, and avoids having to access data sources for it all the time, which would make the user wait for data to reload on such changes. But what about the droid attack on the wookies handling process death then?

ViewModel instances survive configuration changes because they are stored in a static way. This means they are still destroyed on process death, meaning we’ll lose any data that’s in the ViewState. But if we’ve followed the ideas above, there is no data there that we can’t fetch again. As long as we don’t lose the navigation parameters, we can create a new instance of ViewModel when our process is restarted, and fetch data for the ViewState again, using persistent data sources.

This isn’t the end of the story, as applications also have state that isn’t persisted yet. My internal terminology for this kind of temporary state is ephemeral state. This is state that we don’t need to store long term (e.g. across application restarts), but we have it while the application is running, and we want to keep it through process death. Navigation state and user inputs are perhaps the most common example of this.

This state can exist outside the ViewModel, and be stored in savedInstanceState (automatically by the View system, completely manually by placing values in the Bundle, using libraries - whatever your preference). When the process is restarted, persistent state will repopulate the ViewState, and ephemeral state can be filled back in on top of the persistent state by restoring it from savedInstanceState.

Putting this a bit visually, persistent state is stored below, while ephemeral state is saved upwards when needed:

Up and down

Conclusion

That’s all for now! Just to reiterate the disclaimer up top, these are all subjective thoughts on these approaches. Still, I hope someone finds them helpful, and that it makes you think about whether you need any or all of these things in your application.

Check out the next article of the series, Thoughts about Event Handling on Android, which covers the theory of handling one-time events, and important considerations to make when implementing them.

The previous article of this series is Designing and Working with Single View States on Android, which includes both the theory and practical tips for using single view states.

Stay tuned for more articles, which will soon get to actually implementing these ideas.

Thanks to @stewemetal and @Zhuinden for reviewing this article.



You might also like...

Fragment Lifecycles in the Age of Jetpack

Fragments have... Complicated lifecycles, to say the least. Let's take a look at these, and how they all fit into the world of Jetpack today, with LifecycleOwners, LiveData, and coroutines.

Thoughts about Event Handling on Android

In MVVM-like view architectures, view state isn't enough to communicate from the ViewModel to the View. They have to be supplemented by some kind of events, which come with several challenges you should be aware of.

Introducing 🌈RainbowCake

RainbowCake is an Android architecture framework, providing tools and guidance for building modern Android applications.

Designing and Working with Single View States on Android

Describing the state of a screen is a common practice these days thanks to MVI popularizing the concept. Let's take a look at some examples of how you can design your state objects neatly using data classes and sealed classes, and how you can put them into practice.