Bridging the VM and View

Since the Android Architecture components LiveData classes were released, I've been trying to integrate it into an MVVM architecture where the final hop between the ViewModel and View use LiveData for this communication. Why LiveData and not RxJava? The main reason is LiveData was architected specifically with the lifecycle of Android in mind, so it solves many of the issues that happen when attempting to update Android Views when the lifecycle is in the wrong state. In this article, I'm going to spend some time walking through a pattern inspired by a ViewState Machine pattern described in a talk by Amanda Hill at KotlinConf 2017.
ViewState Machines
ViewState is immutable state used to define everything that a View might need to render itself. In Kotlin we'd define a data class, where all of the properties are val
. In order to produce a new state, the data class defines a next(action: Action) method that calculates (copies) the current state and returns a new state object modified by the action passed to it.
sealed class Action {
object OnLoading: Action()
data class OnComic(val url: Uri): Action()
}
data class ComicState(
val showLoading : Boolean = false,
val comicUrl : Uri = null
) {
fun next(action: Action): State =
when(action) {
Action.OnLoading -> {
copy(showLoading = true, comicUrl = null)
}
is Action.OnComic -> {
copy(showLoading = false, comicUrl = action.url)
}
}
}
By combining this with Kotlin sealed classes for the Action adding new actions will easily cause the when
to require developers to add new when clauses if new actions are added to the sealed class, effectively directing the developer about how to maintain the class.
Only ViewModels Change State
So, we now have some state, what do we do with it? The ViewModel will keep the state in a LiveData, and make any changes to it as network calls and so on are made.
In my most recent project, we spent some time making LiveData a little easier to use, by making ourselves a base class.
open class BaseLiveData<T : Any>(initialValue: T) : LiveData<T>() {
init {
super.setValue(initialValue)
}
override fun getValue(): T {
return super.getValue()!!
}
override fun setValue(value: T) {
if (this.value != value) {
super.setValue(value)
} else {
Timber.v("skipping duplicate posted value - %s", value)
}
}
protected open fun update(f: (T) -> T) {
value = f(value)
}
protected open fun post(f: (T) -> T) {
postValue(f(value))
}
}
This helps us to be consistent with the way the live data is updated, and provides a constructor mechanism to set the initial state.
Next we wrote a base mutable LiveData. Which just makes a class that can modify the live data. Specifically we only want the ViewModel to be able to modify the live data, and we want the View to only see a read-only version of the LiveData.
open class BaseMutableLiveData<T : Any>(initialValue: T) : BaseLiveData<T>(initialValue) {
public override fun setValue(value: T) {
super.setValue(value)
}
public override fun update(f: (T) -> T) {
super.update(f)
}
public override fun post(f: (T) -> T) {
super.post(f)
}
}
So, with this, we can define the ViewModel something like this:
class ComicViewModel(
val getCurrentComicUseCase : CurrentComicUseCase
): BaseViewModel() {
private val _state = BaseMutableLiveData(ComicState())
val state : BaseLiveData = _state
}
Here, we've set up the initial state, and have set up a private _state
property that the ViewModel can use to change the state. And have a public state
property that the View can observe for changes.
The ViewModel is then going to make updates to the state as network activity is performed. I'll show making network calls using RxJava here, but that's not necessary, you could use Kotlin coroutines or some other mechanism just as easily.
_state.update { it.next(Action.OnLoading) }
getCurrentComicUseCase.execute(Async)
.subscribeBy(
onSuccess = {
_state.update { s -> s.next(Action.OnComic(it)) }
},
onError = {
TODO("Add some error state")
}
).also {
autoDispose(it)
}
We're using RxKotlin for it's superior subscribeBy()
method which has named parameters to keep our onSuccess
and onError
straight. Just imagine that the autoDispose()
method is provided in the BaseViewModel class (a simple class derived from the Architecture Components ViewModel. Again, shown here is an RxJava UseCase pattern, but you can substitute for coroutines or anything else you'd like.
Isolating State Changes
Having one immutable data class for all of the state of a given view has a drawback: observers are triggered for every change and it can be difficult for the View to understand a minimal set of changes to make the UI. Often this leads to rendering screen values unnecessarily.
You could use DataBinding to help you sort through the changes and only update the parts of the screen that have actually changed. But what if you don't want to use Databinding? I know it's a bit heretical to suggest not using it, but we've found in a few recent projects that it's nice to not have to deal with some of the downsides of Databinding (specifically the complexity it adds to a project that you then have to explain to the rest of the developers).
We've found it liberating to just use Android views, and write the code we want to render the state changes we want to see on the screen. Using a simple mechanism we can break out the parts of the ViewState we want to watch for changes. Using a simple Kotlin extension function, we can filter only distinct changes for part of our state.
fun <T> LiveData<T>.distinct(): LiveData<T> {
val mediatorLiveData: MediatorLiveData<T> = MediatorLiveData()
mediatorLiveData.addSource(this) {
if (it != mediatorLiveData.value) {
mediatorLiveData.value = it
}
}
return mediatorLiveData
}
Then we can use this distinct()
method to make separate properties on our ViewModel that the View can subscribe to individually.
val comicUrl = Transformations.map(_state) { it.comicUrl }
.distinct()
val isLoading = Transformations.map(_state) { it.showLoading }
.distinct()
Effectively, now, whenever the comicUrl
or the showLoading
state properties change, subscribers to these LiveData transformations will be notified.
Views Observing the ViewState
Observing changes to LiveData is as documented, and works as expected. In this case we would have something like this.
viewModel.comicUrl.observe(this, Observer { comicUrl ->
onComicChanged(comicUrl)
})
viewModel.isLoading.observe(this, Observer { isLoading ->
onLoadingChanged(isLoading)
})
The details of how to make the View display these changes is up to you. Using DataBinding you could simply just hook up your ViewModel to the layout using the LiveData integration provided in that library, or you could simply perform a few straightforward view lookups and set some image sources, and show and hide some text or a progress bar. That's completely up to you.
Live data will automatically remove observers at the correct point in the lifecycle, but you can also remove the observers in code. Sometimes you'll need to do that.
Testing ViewState
One very interesting property of using a ViewState Machine pattern to develop your ViewModel to View communications is that you can easily test it. Set up your state, then manipulate it by sending several actions, then verify that the state looks as you'd expect it to. This can be very valuable once the state has grown to represent several different user interactions, or represents several phases of a more complicated on-screen process.
For example, you could use the Spek Kotlin testing framework to perform a few assertions on the ViewState.
class ComicStateSpek : Spek({
describe("comic state") {
given("initial state") {
val state = ComicState()
it("has correct state") {
assertFalse(state.showLoading)
assertNull(state.comicUrl)
}
on("load") {
val state = state.next(Action.OnLoading)
it("should be loading") {
assertTrue(state.showLoading)
}
}
on("comic") {
val state = state.next(Action.OnLoading)
.next(Action.OnComic("https://imgs.xkcd.com/comics/space_mission_hearing.png")
it("should not be loading") {
assertFalse(state.showLoading)
}
it("should have a comic url") {
assertNotNull(state.comicUrl)
}
}
}
}})
Since the state data classes use pure Kotlin, and won't have any Android framework code inside them, they can easily be tested in a JUnit framework such as Spek. So this tests that given certain actions, the state produces matches expectations. This provides a reasonable way to test that the data looks as expected. You can almost trust that if a ViewModel produces this state, the View will know how to render it. Though to be more pragmatic you'll also need ViewModel and View tests.
Conclusion
I hope this article has given you ideas, or inspired you to use a ViewState machine to set up communication between your ViewModel and your View. Happy Android development.