MVI on Android
I have been using Cycle.js for a couple of small hobby projects and I really like the observables only approach. Having used reactive patterns on Android to some extent as well, I was wondering if a similar architecture could be achieved in an Android application. The release of the new Architecture Components from Google finally presented an opportunity to try and re-write and old application of mine with (almost) observables only.
This blog post will focus entirely on my take on how to implement certain ideas from Cycle.js in Android. I will not spend too much time on explaining the patterns themselves. Please check out the documentation on Cyclejs.org if you are not familiar with it. Besides Cycle.js, my approach is also inspired by the MVI series of Hannes Dorfmann and Jake Wharton’s talk on the state of managing state with RxJava.
When I try out new patterns, I usually start off with implementing them for very simple use-cases, but then quickly move on to applying them to something a little more complex. I think you can only really evaluate a certain pattern once you get to the ugly details of a real world application and see how it behaves in those scenarios.
Hence the application I am talking about today features many aspects of a real world Android app: it fetches data from online sources, stores them in a local database and presents it with an overview and a details screen. Although still being relatively simple, I think it serves well as a test-case for a new architectural pattern.
The app features two screens: the first one uses data from TheMovieDb to show movie posters in a grid, either sorted by popularity, rating, date or starred by the user. The second screen shows detail information about a movie. The user can star the it, which saves it to the local SQLite database and thus makes it available offline.
The full source code of the application is available on github.
The basic idea is that all the business logic of the application lives in top level functions, detached from the Android framework. These functions set up an observable stream that emits the current state of the screen. In order to keep this stream alive across configuration changes, it is created in a retained ViewModel from the Architecture Components. The ViewModel offers this state-emitting stream as LiveData to the Activity/Fragment, whose job is to transform the state into something on the screen. Using LiveData is nice because it is lifecycle aware and always replays the latest emission to a new observer. The same could be achieved with pure RxJava, LiveData is just convenient.
I started using Kotlin a couple of months ago and what can I say, it is awesome! If you have not used it yet, I really encourage you to give it a try! Besides Kotlin, the application is heavily based on RxJava and uses the new Architecture Components from Google, namely ViewModel, Room and LiveData. The local SQLite database is created using Room and the REST calls to TheMovieDB use Retrofit.
The code is organized within two main feature packages, grid and a details. Within those, all the top level business logic functions are contained in a component package. This clearly separates them from the code dealing with the Android framework.
The observable streams are modeled using the Model-View-Intent terminology from Cycle.js: every component has intention (intent is kind of already taken in Android…) and model functions and returns a state-emitting Observable. Yes, the V is missing. Due to the way the Android framework is currently built, using a view function like in Cycle.js is difficult. Instead the Activity/Fragment subscribes to the state stream and renders each emission on the screen.
Every component features a main function. It takes as input a sources object that contains — as the name implies — all possible sources of data for the stream. This includes abstractions for getting data from the network, the local database and shared preferences. Lastly and most importantly the sources include all the UI events, e.g. the user clicking on a movie poster or scrolling down to load more data.
Note the usage of Relays here. As the stream is kept alive across configuration changes, the views cannot be directly bound to it, as they are recreated after a configuration change.
The intention function transforms these UI events into actions, modeled in a sealed class. This provides a nice abstraction for the model function, as it does not have to deal with UI events directly, but simply receives a stream of actions. This is also useful for testing as you will see later on. A shortened version of the grid intention function portrays the idea.
The model function takes these actions as input, acts accordingly and produces a new state. It does so not by directly mutating the current state but by creating a new one every time and incorporating changes using reducer functions. If you have used Redux or cycle-onionify on the web, this pattern will look very familiar to you. Again using a shortened version of the grid model function to illustrate:
A quick note on navigation: instead of putting such actions directly into the state object (e.g. a showDetailsFragment: Boolean), I opted to have a separate stream for them. This frees me from having to emit events like detailsFragmentShown. The Activity subscribes to this navigation stream as it does to the state stream and acts accordingly on every emission.
Note that an approach following Cycle.js more purely would take this further and create so-called drivers not only for navigation, but for everything that performs a side-effect with the outside world. You could do this here as well and remove HTTP or SQL requests from the main model stream. Instead you would create separate streams for every effect and feed the results as an additional source into the main model stream. This makes testing a bit easier as you would not need to mock anything to test the main model stream. For more information on this driver pattern, please visit the Cycle.js documentation: https://cycle.js.org/drivers.html
The Activity/Fragment observes the LiveData from the ViewModel and on every emission updates the view with latest state. This update happens via the DataBinding library, using something I call a ViewDataObject. These are mutable and bound to the views. Whenever the Activity/Fragment observers a new state emission, it changes the fields in the corresponding ViewDataObject and the views update automatically thanks to DataBinding. Instead of using DataBinding, you could of course also directly set the values on the view. DataBinding ist just convenient because it handles things like not setting a new value on a view if it is identical to the old one.
For the navigation stream, using LiveData is not appropriate because you obviously do not want the replay-latest feature in this case. Thus I am using plain RxJava. This requires some manual lifecycle handling, but that is actually super easy to do with the LifecycleRegistry and a Kotlin extension function:
As all the core business logic is contained in top level functions that take a stream of observables as input and output a stream of observables, testing becomes relatively simple. You only need to mock the service that fetches movies online from TheMovieDB and the DAOs accessing the local SQLite database. I opted to separately test the model, intention and navigation functions of each component. An example test from the grid component’s model function looks like the following (some class properties and helper functions are omitted, but you will get the idea):
Head over to the github repo to see the complete test classes. Note that the details component is currently lacking tests, they will follow.
Overall I am pretty happy with how this experiment turned out. The explicit modeling of state with the observables stream is really enjoyable to read and you get a good sense of what the application is doing by just reading the flow from top to bottom. Compare that to the previous implementation of the same application, where you had to jump around all the time to find out what is happening in reaction to what, etc. Testing the main business logic also gets quite simple, as you are only dealing with plain functions!
But the setup does require a not unsubstantial amount of boilerplate code just to get something on the screen. Whether that makes sense or not totally depends on the scope of the application I think. If you just want to get something out quickly, it probably is not worth your time. But as soon as you are trying to build something more sustainable and maintainable, I really think using such a pattern can reduce bugs and improve maintainability. And maybe over time, some kind of framework evolves that helps with the boilerplate.
One thing I do not really like in the current implementation is how the views have to imperatively call next on the relays on every event. One alternative would be to have the relays subscribe to the views. But this subscription would need to be managed on configuration changes because the relays are retained across configuration changes and the views are obviously not, making it more complex. If you have good ideas how to make this event handling prettier, please let me know!
A proper tablet mode is currently missing in the application. Implementing this will probably require some refactoring. Currently the state of a component is contained within one activity. And in tablet mode, the state from the details screen would need to be present on the grid screen. This also touches on the subject of how easily these components can be composed. I am still thinking about the best approach here.
Another thing that is omitted at the moment is the proper handling of process death. But this would actually be relatively easy to fix as the main functions take the initial state as a parameter. Hence the Activity/Fragment could save and read the current state using the savedInstance bundle and pass the result to the main function as the initial state.
That is it! I hope I was able to give you an overview of this experiment I have been working on. Please have a look at the code on github and if you have suggestions/feedback/criticism, let me have it! Thanks for reading.