Reproducible history on Android

Have you ever wondered how you can write your app in a way that you know, at anytime, which was the previous state? Being able to undo an action, a user has previously done, is always a challenging thing and requires conscious effort from the developer.


Gmail Undo action

Applications like Gmail or Google’s Keep support undo action.

“Traditional” app architectures, like MVC or MVP, support these actions but they don’t manage state elegantly. The main reason is that they have an implicit data flow, which means that you can possible end up in invalid states. In this article, we will see how you can avoid these states and define a state machine in your app.


What we really need is to represent state in such a way that will be trackable, immutable and serializable. This state should be our source of truth for the UI at any certain point of time. Every user interaction should render a new different state. Every new state that is produced is our way to: go back, undo and generally reproduce history.

Architectures that enforce this pattern and eliminate the previously mentioned limitations of the “traditional” architectures, are MVI (Model-View-Intent), Redux and generally reactive architectures. For the sake of simplicity, I will analyze the MVI architecture.


Model-View-Intent

Model-View-Intent architecture might seem very strange at first glance! By referring to Intent we don’t mean Android’s Intent, rather than a user’s interaction with our UI. Model is an object that processes information and keeps our state. View is the representation of the UI and cannot be rendered if Model is not present.

This architecture is heavily based on streams. If you use RxJava, every intent should be expressed as an Observable stream of events. For example, when a user clicks a button, you need to “translate” this to an Observable. Lucky for you, there is RxBinding that handles all of these translations.

MVI enforces unidirectional data flow. In a few words, unidirectional data flow provides predictable application state. This means, that a change on the UI layer will trigger an action on the data layer and the changes, that might occur, will transform the UI. This does not necessarily mean that the view directly affects storages etc. In particular View behaves as a pure function in which the same Model has the same output on the UI, without any side effects. As a result, when a bug occurs on the app, you can pinpoint it easier because data follows a “sequence” and the views are loosely coupled from the app data.

Ok, but how can I reproduce history?

Let’s say we have an app that does one thing: let’s you search GitHub users. The viewmodel for this simple app has 3 predefined states: Loading, Error and Success. Whenever user searches, we render the predictable state of Loading. If user found on GitHub, we render the Success state, otherwise something went wrong and we end up with Error state.

On the sample project, I use a cache that saves all these viewmodels and by using a slider, you can “time travel” over them.

So, we have the following View:

interface MainScreenView {
fun searchIntent(): Observable<String>
fun historyIntent(): Observable<Int>

fun render(viewModel: MainScreenViewModel)
fun renderFromHistory(viewModel: MainScreenViewModel)
}

Nothing difficult here. The searchIntent() is the Observable stream that gives us emissions of the search terms. The historyIntent() is the Observable stream from the Seekbar’s progress, which “travels” us back to previous/next states.

Our Model is the following:

data class MainScreenViewModel(
val searchTerm: String,
val
showLoading: Boolean,
val showFields: Boolean,
val showError: Boolean,
val gitHubUser: GitHubUser? = null,
val errorMessage: String? = null
) {

val createdAt: Long = System.currentTimeMillis()

companion object {
fun inProgress(searchTerm: String): MainScreenViewModel = MainScreenViewModel(searchTerm, true, false, false)
    fun success(searchTerm: String, gitHubUser: GitHubUser) =    MainScreenViewModel(searchTerm, false, true, false, gitHubUser)
    fun error(searchTerm: String): MainScreenViewModel = MainScreenViewModel(searchTerm, false, false, true, null,
"Houston we have a problem")
}
}

In our example, viewmodel is a data class because we want immutability! We actually don’t want someone else touching our state! The createdAt property exists in case we want to sort these viewmodels by time.

Finally, our Presenter is nothing special as well. I removed most of the lines, in order to focus on the most significant method: render(). Whenever the response is success, we map its result with the following factory method: success(it). Whenever the response is failure (mostly 404 Http status code- user not found), we use the error() factory method. Before that we emit an item that the request is in flight, using the inProgress() factory method. We save its state on dispatchRender(). The method doOnNext() is actually very helpful for side effects like logging, so we log our render events. Note: I could use the scan() operator, but I wanted to keep it simple.

class MainScreenPresenter {

...

fun bindIntents() {
val searchIntent = viewRef.get()?.searchIntent()?.share() ?: Observable.empty()

searchIntent
.debounce(400, MILLISECONDS, AndroidSchedulers.mainThread())
.doOnNext { Log.d("Intent", "Received Search Intent") }
.switchMap {
gitHubApi.githubDAO
.searchUser(it)
.map { MainScreenViewModel.success(it) }
.onErrorReturn { MainScreenViewModel.error() }
.startWith(MainScreenViewModel.inProgress())
}
.doOnNext { Log.d("Render", it.toString()) }
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<MainScreenViewModel>() {
          override fun onNext(viewModel: MainScreenViewModel) {
dispatchRender(viewModel)
}

override fun onError(e: Throwable) {
Log.wtf("ragment", e.message)
}

override fun onComplete() {
}

})
.addTo(compositeDisposable)
}
private fun dispatchRender(viewModel: MainScreenViewModel) {
ViewModelCache.append(viewModel)
viewRef.get()?.render(viewModel)
}
...
}

The View’s implementation is like this:

progressbar.showOrHideInvisible(viewModel.showLoading)
userName.showOrHideInvisible(viewModel.showFields)
userLogin.showOrHideInvisible(viewModel.showFields)
gitHubUrl.showOrHideInvisible(viewModel.showFields)

userName.text = viewModel.gitHubUser?.name
userLogin.text = viewModel.gitHubUser?.login
gitHubUrl.text = viewModel.gitHubUser?.url

if (viewModel.showError) {
Toast.makeText(this, viewModel.errorMessage, Toast.LENGTH_LONG).show()
}

Actually this is the only pain-point of this pattern. You somehow need to smartly and effectively invalidate Android’s views without worrying about how many layout passes our ViewGroup has executed. In an upcoming Medium post we will discuss on how to fix these issues.

Actually that’s all! You see? Nothing too hard. The result of the above looks like this:

Time travel on Sample app

Debugging and testing with these reactive architectures seems to me very straightforward. By using Kotlin’s data classes or Google’s AutoValue or even your own custom implementation, you can compare these states and due to the fact that state must be immutable, you know that no one is going to change it! Also this is your single source of truth.

So, the only thing you have to emulate in unit tests are Intents. You can achieve this by mocking them with PublishSubjects. Now you can use those to “fake” user interactions and then just assert the output of your equivalent render method. Unidirectional data flow and pure functions help you a lot on this field.

As a bonus, you can have a stack of the last n rendered viewmodels. This is going to help you when reporting bugs e.g. on your Crashlytics, Bugsnag etc platform. It’s like a time travel. By feeding these last n viewmodels on the render() method you can see what led to your app crashing. That’s because you know the n-1 viewmodel and what is really going on the screen. Do you remember the doOnNext method? You can always add more info on your Observable streams, for example an Intent for infinite scroll or pull to refresh.


In more complicated apps, you can make your app’s logic look very simple. By plug-and-playing with Observable streams and with all these various Rx operators, you have the ability to achieve the most extreme scenarios. That’s very convenient because you can almost see all of the edge cases that can happen and fix them before you release your app in production. Moreover, your data flow becomes explicit. That’s because you know the exact state your screen is currently in and how a user’s action will affect its current state.

Redux works somehow on the same style. Redux introduces Reducers, Actions, Dispatchers and Store. In a nutshell, Reducers work as pure functions from previous state to the new one, Actions are what a user has done(pretty much like our Intents), Dispatchers are like an event bus for all those Actions and Store manages the state and acts like an orchestrator. Finally, there are Middlewares which let you extend Store with some additional functionality, (e.g. like what we did on doOnNext() with logging)


You can find the sample GitHub project here.

Thanks to Pavlos-Petros Tournaris, Kostas Kremizas and Stratos Pavlakis for providing me feedback for this article.