One of the more interesting changes that has happened in the last few years is the appearance of state management libraries like Redux, Flux or MobX. This has generated a healthy debate amongst developers about — essentially — how to best make sense of the ever growing complexity in software.
Redux & co are there to help you make sense and cut down the complexity that arises from state and state changes. Redux specifically does that by enforcing a few simple rules:
- there is only one source of truth, the state
- that state is immutable
- new state can only be created by pure functions called reducers as a result of some previous action
In mobile, a lot of state resides in the UI layer, sometimes explicit, sometimes implicit. Things like the text value of a label, the enabled or disabled state of a button or the hidden or visible status of an image are all essentially expressions of some state in the system.
Unfortunately, all too often I see UI code that looks like this:
This is not bad in and of itself. It does what it’s supposed to do and you might argue that’s good enough. And indeed, for a simple view, this doesn’t stand out as bad. But there’s a subtle problem: it mixes business layer state with view layer state.
Every time we write code like this we’re making our system harder to reason about. In the above example, a single button’s selected or unselected state depends on a variety of factors, spread out throughout the code. Clearly it will only get worse the more complex the view gets and at one point it will stop scaling. When that happens, the cognitive load needed to properly understand all possible code paths becomes too great.
In this post I’m going to show the way I think about UI state. The fact that I mentioned Redux at the start isn’t a coincidence. I’m going to take some best practices and learnings from there in order to build a View as a function of its state. In this way it’ll be easier to reason about, more deterministic and simpler to test.
For start, let’s get a visual idea of what the view will look like:
You can see it’s supposed to display a user’s profile image and name, as well as a telephone number and a follow button to indicate favourite users. If you tap on the button you can add or remove a user from your favourites. If you tap on the phone number, you can start a phone call with your contact.
Before we jump any further, let’s write out how this view will look like in code:
Now, I said above one of the key pillars of Redux is that state is immutable and can only be created by pure functions called reducers. Essentially, this:
Does that mean that building views as a function of state implies creating a new instance each time something changes? On mobile, that would be a very expensive thing to do. What we can do, instead, is think about the full state of the view and redraw it only based on that. So if we look at the layout above, what state do we have? At it’s simplest, it’s this:
- a URI resource for the contact’s profile image
- a string for the user name
- a string for the phone number
- and a local image resource for the favourites button
We can write it in code like this:
In the above example, the view tells the outside world how it thinks its state should look like, using Inversion of Control.
The state is also tailor made for the view. It contains only the information directly needed by the view in order to render itself and nothing more.
With that in place, it’s just a matter of creating a method that redraws the view using a given ViewState:
You can see we’ve ended up with a list of simple, deterministic, assignments. There is no other code in the view that changes what gets shown on screen.
Testing this with Espresso or any other UI testing framework becomes extremely easy.
In fact, we’ve also decoupled a lot of business logic that previously existed in the View by letting the ViewState deal with converting some data layer object into a format that’s appropriate for the View. We can also unit test that separately.
However, in the initial example we could also add or remove a contact from our favourites list, with the corresponding change in UI.
In order to re-enable that functionality and keep the deterministic redraw mechanism in place, we can add a new interface, called Interactor:
Essentially, this mechanism allows the View to tell the outside world (Activities, Fragments, ViewModels, etc) that it would want some sort of operation performed. In practice this can look like this:
From here on, the view delegates these responsibilities to whatever class implements the Interactor interface. As before, testing the Interactor becomes extremely easy using both Espresso and Mockito.
In conclusion, we’ve created a ContactView that redraws as a function of some state. We’ve done that by grouping all redraw operations in one method and moving all other logic outside of the view, as part of an Interactor.
The full code of our view looks like this:
And now that we have the full picture of all the changes we did to our initial example, we can see an unintended good side-effect is that we’re in a much better position to handle the complexity of our view as it grows and changes.
If we need to add more subviews like address fields, workspace, etc, we only need to change a few lines in the redraw method. Likewise, if we need to extend the capabilities of the view, we only need to add more methods to the Interactor.
This article is one of a multi-part series exploring State, UI and Redux and how we can leverage knowledge of these topics to write more modular and testable code.