Reactive Apps with Model-View-Intent - Part3 - State Reducer
by Hannes Dorfmann —
In the previous part we have discussed how to implement a simple screen with the Model-View-Intent pattern with an unidirectional data flow. In this blog post we are going to build a more complex screen with MVI with the help of a state reducer.
If you haven’t read part 2 yet, you should read that before continue with this blog post, because there is described how we connect the View via Presenter with the business logic and how data flows unidirectional.
Now let’s build a more complex screen like this:
As you see in the video above this screen displays a list of items (products) grouped by category. The app only displays 3 items for each category and the user can click on a “load more button” to load all items of that category (http request). Additionally, the user can do pull-to-refresh and once the user has scrolled down to the end of the list more categories are loaded (pagination). Of course all this actions can be executed simultaneously and each of them could also fail (i.e. no internet connection).
Let’s implement this step by step. First, let’s define the View interface.
The concrete View implementation is pretty straight forward and therefore I won’t show the code here (can be found on github). Next let’s focus on the Model. As already said in previous posts the Model should reflect the State. So let’s introduce a Model called HomeViewState:
Note that FeedItem is just a interface every item has to implement that is displayable by the RecyclerView. For example Product implements FeedItem.
Also the category title displayed in the recycler SectionHeader implements FeedItem.
The UI element that indicates that more items of that category can be loaded is a FeedItem and holds internally it’s own little State to indicate whether or not we are loading more Items of a certain category:
And last but not least there is a business logic component HomeFeedLoader responsible to load FeedItems:
Now let’s connect the dots step by step in our Presenter. Please note that some code shown here as part of the Presenter should rather be moved into an Interactor in a real world application (which I didn’t for the sake of better readability).
First, lets start with loading the initial data:
So far so good, no big difference to how we have implemented the “search screen” as described in part 2. Now let’s try to add support for pull-to-refresh.
Use Observable.merge() to merge together multiple intents
But wait: feedLoader.loadNewestPage() only returns “newer” items but what about the previous items we have already loaded?
In “traditional” MVP one would call something like view.addNewItems(newItems) but we have already discussed in part 1 why this is a bad idea (“The State Problem”).
The problem we are facing now is that pull-to-refresh depends on the previous HomeViewState since we want to “merge” the previous items with the items returned from pull-to-refresh.
Ladies and Gentlemen please give a warm welcome to the STATE REDUCER
State Reducer is a concept from functional programming that takes the previous state as input and computes a new state from the previous state like this:
The idea is that such a reduce() function combines the previous state with foo to compute a new state. Foo typically represents the changes we want to apply to the previous state.
In our case we want to “reduce” the previous HomeViewState (originally computed from loadFirstPageIntent) with the result from pull-to-refresh. Guess what, RxJava has an operator for that called scan(). Let’s refactor our code a little bit.
We have to introduce another class representing the partial change (the thing we have called Foo in the previous code snipped) that will be used to compute the new state.
So what we did here is, that each Intent now returns an Observable<PartialState> rather then directly Observable<HomeViewState>.
Then we merge them all into one observable stream with Observable.merge() and finally apply the reducer function (Observable.scan()).
Basically what this means is that, whenever the user starts an intent, this intent will produce PartialState objects which then will be “reduced” to a HomeViewState that then eventually will be displayed in the View (HomeView.render(HomeViewState)).
The only missing part is the state reducer function itself. The HomeViewState class itself hasn’t changed (scroll up to see the class definition), but we have added a Builder (Builder pattern) so that we can create new HomeViewState objects in a convenient way.
So let’s implement the state reducer function:
I know, all these instanceof checks are not super pretty but that is not the point of this blog post. Why does technical bloggers write “ugly” code like the one shown above? It’s because we want to make a point on a certain topic without requiring the reader to have a full mental model of the source code i.e. of our shopping cart app nor prior knowledge of certain design patterns.
Therefore, I think it is better to avoid design patterns in blog posts which would produce nicer code but may lead to harder readable blog posts.
The focus of this blog post is set on state reducer. By looking at the state reducer with instanceof checks everybody can understand what the reducer does.
Should you use instanceof checks in your app?
No, use design patterns or other solutions like defining PartialState as interface with a method like public HomeViewState computeNewState(previousState).
In general you may find RxSealedUnions by Paco Estevez useful when building apps with MVI.
Alright, I think you get the idea how a state reducer works. Let’s implement the remaining features as well: Pagination and the ability to load more items of a certain category:
Implementing pagination (loading next “page” of items) is pretty the same as pull-to-refresh except that we are adding the loaded items at the end of the list instead of the top of the list as we do with pull-to-refresh.
More interesting is how we deal with loading more items of a certain category. Well, for displaying a loading indicator and an error / retry button for a given category we only have to search for the corresponding AdditionalItemsLoadable object in the list of all FeedItems.
Then we change that item to either show loading indicator or error / retry button.
If we have loaded all items of a certain category successfully we search for the SectionHeader and AdditionalItemsLoadable and replace all items in between with the newly loaded items. That’s it.
The aim of this blog post was to show you how a state reducer can help us to build complex screens with very little and understandable code.
Just step back and think how you would have implemented that with “traditional” MVP or MVVM without a state reducer?
The key to be able to use a state reducer is obviously that we have a Model class that is reflecting the State. Therefore, it was very important to understand what a Model actually is as described in the first part of this blog post series.
Also, a state reducer can only be used if we are sure that the State (or Model to be precise) comes from a single source of truth. Therefore, a unidirectional data flow is very important.
I hope that now it makes more sense why we have spend part 1 and part 2 on these topics and that you now got this “aha” moment where all the dots connect together.
If not, no worries, it took quite some time for me too (and a lot of practice and a lot of mistakes and retries).
You may be wondering why we haven’t used a state reducer for the “Search Screen” (see part 2).
State Reducer make mostly sense if we are depending on the previous state somehow. In the “Search Screen” we are not depending on the previous state.
Last but not least, I would like to point out, if you haven’t noticed that yet (without going to much into details), that all our data is immutable (we always create a new HomeViewState, we never call a setter method on any object).
Therefore, also mutli-threading is super easy.
The user can start pull-to-refresh at the same time as loading the next page and load more items of a certain category because the state reducer is able to produce the correct state without depending on any particular order of the http responses.
Additionally, we have written our code with pure functions, no side-effects. This makes our code super testable, reproducible, simple to reason about and highly parallelizable (mutli-threading).
Of course state reducer wasn’t invented for MVI.
You find the concept of a state reducer in many other libraries, frameworks and systems across multiple programming languages.
A state reducer fits perfectly into the philosophy of Model-View-Intent with an unidirectional data flow and a Model representing the State.
In the next part we are focusing on how to build reusable and reactive UI components with MVI.
This post is part of the blog post series "Reactive Apps with Model-View-Intent". Here is the Table of Content: