From MVP to MVI — Model-View-Intent Journey Part I
It seems to me that we, Android devs, are on the never-ending lookout for the perfect architecture pattern. Google Play is full of apps written based on MVP or MVVM, but despite a proven track record of those, I always ask myself after successful release whether it could be done better. Or maybe faster? Using less boilerplate? Less verbosely? More clearly? Could I have improved testability even further? Combining that curiosity with Untitled Kingdom’s tendency to fall in love with the latest hot stuff from Android world, we decided to share our experience gained during implementation of Model-View-Intent architecture pattern. In this part, I want to focus on the case of refactoring simple application from MVP to MVI. If you are willing to dive a little deeper, then you’ll be happy to notice that this article also contains links to many useful resources. By the end of this post you’ll hopefully grasp the basic idea.
The app we’ll be refactoring is yet another list fetching app — this time it’s SpaceX rockets. Pressing the button results in items download and if something goes wrong an error appears.
For those familiar with MVP pattern, this should be straightforward. Firstly, we define an interface, which MainActivity will implement. It will help us decouple platform specific entities from the ones, that could be easily tested using JVM.
In many app implementations Interactor is a common class, which handles data. Similarly here, our business logic lies behind MainInteractor, or to be more specific behind reactive method fetchRocketList , which returns an Observable stream of rockets.
To perform all necessary actions, we need two methods in our MainPresenter file. One that will bind to desired view and the other responsible for manipulating UI upon successful (or erroneous) list fetching.
To fill in the blanks, we need to invoke those two methods in MainAcitivity. Naturally, bind should be called as early as possible (preferably inside onCreate ) and getRocketList whenever user clicks on “SHOW ME ROCKETS!” button.
Let’s start refactoring! Thinking in Model-View-Intent is largely based on thinking in terms of states, which are immutable. Immutability allows for existence of a single valid object to determine how UI will be represented. So, what states SpaceXMVI app has? At any given moment, it can be either in progress, in error or in success state (when list is fetched successfully). Kotlin makes it easy to model that using Data Class.
We can now limit responsibility of MainView to only being able to render MainViewState.
Now I can hear you saying “What about showProgress , showError and showRocketList methods? I spend so much time writing them :(“. No worries, we can make use of each one while implementing render in MainActivity. Stop whining and have a look at below gist:
Let’s increase capabilities of MainView (but not too much, though!). Fine, it can render states quite nicely, but MainPresenter also needs information about user interactions with UI e. g. button clicks, which can be sent using reactive streaming (with a small help of RxBinding library).
By now you should notice that MVI’s view can basically do two things: render (state, which is passed to it) and emit (user actions, also called intents e. g. button clicks). Notice how those two are independent from each other and comprise entrance and exit paths to MainActivity (which is “polluted” by Android dependencies).
One thing, that helped me understand flow of this architecture pattern, was decomposing MainViewState’s fields into parts, namely PartialMainViewStates. This time Kotlin’s Sealed Class fits perfectly. Those partial changes are put back together later inside MainPresenter’s private method reduce, after they are emitted from MainInteractor (Tip: Inside interactor, just map fetched list to PartialMainViewState.ListFetchedState() and consider it a wrapper around previously returned List<Rocket>. For proper emission of ErrorState, be sure to use onErrorReturn since our data stream should never terminate).
We’re getting closer to having something workable, though sort of naive. Last step is refactoring MainPresenter. We won’t need any additional public methods except for bind, but this time its responsibility grows — no more saving MainView object into private field, but get ready for a lot of RxJava stuff. In MVI, presenter is simply mapping user events (intents) to partial states (received from Interactor class), which are reduced to main state (using reduce) and pushed to view’s render method. In below gist notice how I start each list fetching call with ProgressState, that later gets cancelled (Tip: Be careful there. If you want to know why, check out this post by Filip Radoń).
Just call bind inside onCreate and it should work…somehow.
The focus of this section is on dissecting naivety of the introduced implementation. In the end, we’ll come up with a functional version, that even gracefully handles configuration changes.
SpaceXMVI, with its 3-state logic, is a trivial example, I admit. However, for demonstration purposes, I want to show you how to handle multiple UI event streams problem and complex main state creation problem. The first one is actually simple to solve using merge operator, but it’s the second, that tends to be more tricky. Sometimes in reduce method, you’ll be forced to not only create new main state each time, but also to copy values from the previous one. Hence the function signature should be changed:
In essence, we want to look at a partial states stream and accumulate both current and new partial state into a new main state (using our accumulator function reduce ). The perfect solution seems to be scan operator. Below you’ll find slightly changed implementation of MainPresenter, which is better suited for modification, but first be sure to play around with scan marble diagram to grasp the idea fully.
Are you a nice Android developer? If yes, then I bet you always remember about unsubscribing from your streams, don’t you? Let’s now make use of CompositeDisposable!
onDestroy, due to its unstable nature, is not a perfect place for unbind call. The next best guess is, quite obviously, onStop. Yeah. It makes sense. You bind in onStart and unbind whenever activity is stopped (start-stop loop instead of create-destroy one). You know exactly when that happens. What could possibly go wrong?
Well, because of lack of any state saving mechanism, after simple lifecycle event, like turning your display on and off when app is launched, we go back to square one — empty, initial MainViewState (even when we previously fetched entire list). Looking at the current implementation of bind it’s easy to conclude why that happens. Upon each binding (each call to onStart), presenter subscribes again to MainView, forgetting about any previous UI state, and renders whatever default value it has inside scan. To solve this let’s create a proxy BehaviorSubject, which by design emits last state every time observer re-subscribes to it. Take a closer look at a call to subscribeWith, which not only sets a subject proxy to given stream, but also returns that proxy (and saves it to buttonClickObservable. Each action performed on buttonClickObservable is now equivalent to modifying stateSubject).
Could it be more perfect? Actually yes, it could. In current implementation of MVI, the only thing that stops us from preserving state during configuration change is MainPresenter, which is tied to Activity lifecycle. Although in previous years I read many great articles (like this one) about survival of presenter when user e. g. rotates screen, I consider latest ViewModel from Google a proper and elegant solution. I think that this diagram sums it all nicely:
Incorporation of this architecture component is as simple as adding few dependencies to Gradle and inheriting from ViewModel class. Notice that I also renamed MainPresenter to MainViewModel for consistency.
By now you should have a nice working implementation of Model-View-Intent architecture pattern — SpaceXMVI behaves exactly the same as it did on MVP + it handles configuration changes (it is not, however, resistant to state loss when Don’t keep activities developer setting is turned on).
Summing up, I want to thank Luke Cisek and Filip Radoń for their insight and advice. Guys, I’m sure I’m not the only one, who awaits next posts from you about our MVI Journey ;).