Reactive applications with Model-View-Intent. Part 3: State Reducer
- Transfer

In the previous part, we discussed how to implement a simple screen with a Model-View-Intent pattern that uses a unidirectional data stream. In the third part, we will build a more complex screen with MVI using State Reducer.
If you have not read the second part yet, you should do it before further reading, because it describes how we connected View with business logic through Presenter and how the data moves in one direction.
Now let's create a more complex screen:
As you can see, this screen displays a list of items (products) grouped by category. The application displays only 3 elements of each category and the user can click "Download more" to download all products of the selected category (http-request). Also, the user can do Pull-To-Refresh, and when he gets to the end of the list, more categories will be loaded (pagination ) Of course, all these actions can be performed at the same time, and each of them may not be performed (for example, in the absence of the Internet)
Let's implement this step by step. First, let's define the View interface.
public interface HomeView {
public Observable loadFirstPageIntent();
public Observable loadNextPageIntent();
public Observable pullToRefreshIntent();
public Observable loadAllProductsFromCategoryIntent();
public void render(HomeViewState viewState);
}
The implementation of View is fairly straightforward, and therefore I will not show the code here (can be found on github ).
Now let's focus on the model. As mentioned in the previous parts, the model should reflect the state. So, I present to you a model called HomeViewState .
public final class HomeViewState {
private final boolean loadingFirstPage;
private final Throwable firstPageError;
private final List data;
private final boolean loadingNextPage;
private final Throwable nextPageError;
private final boolean loadingPullToRefresh;
private final Throwable pullToRefreshError;
// ... конструктор ...
// ... геттеры ...
}
Please note that FeedItem is just an interface that every element displayed in RecyclerView should implement. For example, Product implements FeedItem . SectionHeader also displays the FeedItem category display name . The UI element that shows that additional category elements can be loaded is FeedItem and contains its state inside in order to display whether we are loading additional elements in a certain category:
public class AdditionalItemsLoadable implements FeedItem {
private final int moreItemsAvailableCount;
private final String categoryName;
private final boolean loading;
private final Throwable loadingError;
// ... конструктор ...
// ... геттеры ...
}
And also create the HomeFeedLoader business logic element , which is responsible for loading FeedItems :
public class HomeFeedLoader {
public Observable> loadNewestPage() { ... }
public Observable> loadFirstPage() { ... }
public Observable> loadNextPage() { ... }
public Observable> loadProductsOfCategory(String categoryName) { ... }
}
Now let's connect everything step by step in our Presenter. Keep in mind that some of the code presented here as part of the Presenter should most likely be ported to Interactor in a real application (which I did not do for better readability). First, load the initial data:
class HomePresenter extends MviBasePresenter {
private final HomeFeedLoader feedLoader;
@Override
protected void bindIntents() {
Observable loadFirstPage = intent(HomeView::loadFirstPageIntent)
.flatMap(ignored -> feedLoader.loadFirstPage()
.map(items -> new HomeViewState(items, false, null) )
.startWith(new HomeViewState(emptyList, true, null) )
.onErrorReturn(error -> new HomeViewState(emptyList, false, error))
subscribeViewState(loadFirstPage, HomeView::render);
}
}
While everything is going well, there are no big differences with how we implemented the “search screen” in part 2. Now let's try adding Pull-To-Refresh support:
class HomePresenter extends MviBasePresenter {
private final HomeFeedLoader feedLoader;
@Override
protected void bindIntents() {
Observable loadFirstPage = ... ;
Observable pullToRefresh = intent(HomeView::pullToRefreshIntent)
.flatMap(ignored -> feedLoader.loadNewestPage()
.map( items -> new HomeViewState(...))
.startWith(new HomeViewState(...))
.onErrorReturn(error -> new HomeViewState(...)));
Observable allIntents = Observable.merge(loadFirstPage, pullToRefresh);
subscribeViewState(allIntents, HomeView::render);
}
}
But wait: feedLoader.loadNewestPage () returns only new elements, but what about the previous elements that we downloaded earlier? In the “traditional” MVP, someone can do view.addNewItems (newItems) , but in the first part we already discussed why this is a bad idea (“State Problem”) The problem we are facing now is this: Pull-To-Refresh depends on the previous HomeViewState, since we want to combine the previous elements with elements that are returned from Pull-To-Refresh.
Ladies and gentlemen, please love and favor - State Reducer

State Reducer is a concept from functional programming. It takes the previous state to the input and calculates the new state from the previous state:
public State reduce( State previous, Foo foo ){
State newState;
// ... вычисление нового State на основании предыдущего с применением Foo
return newState;
}
The idea is that the reduce () method combines the previous state with foo to calculate the new state. Foo usually represents the changes that we want to apply to the previous state. In our case, we want to combine the previous HomeViewState (originally obtained from loadFirstPageIntent) with the results from Pull-To-Refresh. It turns out that RxJava has a special operator for this - scan () . Let's change our code a bit. We need to create another class that will reflect a partial change (in the code above it is called Foo) and used to calculate the new state:
Homeresenter
class HomePresenter extends MviBasePresenter {
private final HomeFeedLoader feedLoader;
@Override
protected void bindIntents() {
Observable loadFirstPage = intent(HomeView::loadFirstPageIntent)
.flatMap(ignored -> feedLoader.loadFirstPage()
.map(items -> new PartialState.FirstPageData(items) )
.startWith(new PartialState.FirstPageLoading(true) )
.onErrorReturn(error -> new PartialState.FirstPageError(error))
Observable pullToRefresh = intent(HomeView::pullToRefreshIntent)
.flatMap(ignored -> feedLoader.loadNewestPage()
.map( items -> new PartialState.PullToRefreshData(items)
.startWith(new PartialState.PullToRefreshLoading(true)))
.onErrorReturn(error -> new PartialState.PullToRefreshError(error)));
Observable allIntents = Observable.merge(loadFirstPage, pullToRefresh);
HomeViewState initialState = ... ; // Показать загрузку первой страницы
Observable stateObservable = allIntents.scan(initialState, this::viewStateReducer)
subscribeViewState(stateObservable, HomeView::render);
}
private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
...
}
}
Now every Intent returns an Observable
viewStateReducer
private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
if (changes instanceof PartialState.FirstPageLoading)
return previousState.toBuilder()
.firstPageLoading(true)
.firstPageError(null)
.build()
if (changes instanceof PartialState.FirstPageError)
return previousState.builder()
.firstPageLoading(false)
.firstPageError(((PartialState.FirstPageError) changes).getError())
.build();
if (changes instanceof PartialState.FirstPageLoaded)
return previousState.builder()
.firstPageLoading(false)
.firstPageError(null)
.data(((PartialState.FirstPageLoaded) changes).getData())
.build();
if (changes instanceof PartialState.PullToRefreshLoading)
return previousState.builder()
.pullToRefreshLoading(true)
.nextPageError(null)
.build();
if (changes instanceof PartialState.PullToRefreshError)
return previousState.builder()
.pullToRefreshLoading(false) // Hide pull to refresh indicator
.pullToRefreshError(((PartialState.PullToRefreshError) changes).getError())
.build();
if (changes instanceof PartialState.PullToRefreshData) {
List data = new ArrayList<>();
data.addAll(((PullToRefreshData) changes).getData());
data.addAll(previousState.getData());
return previousState.builder()
.pullToRefreshLoading(false)
.pullToRefreshError(null)
.data(data)
.build();
}
throw new IllegalStateException("Don't know how to reduce the partial state " + changes);
}
I know that all of these instanceof checks are not very good, but that is not the point of this article. Why do technical bloggers write “bad” code, as in the example above? Because we want to concentrate on a certain topic without forcing the reader to keep in mind the entire source code (for example, our application with a basket of goods) or to know certain design patterns. Therefore, I believe that it is better to avoid patterns in the article that will make the code better, but can also lead to worse readability. The focus of this article is State Reducer. Looking at him with instanceof checks, anyone can understand what he is doing. Should you use instanceof checks in your application? No, use design patterns or other solutions. For example, You can declare PartialState an interface with the public HomeViewState computeNewState (previousState) method. In general, you can findRxSealedUnions from Paco Estevez useful when you develop with MVI applications.
Okay, I think you get the idea of State Reducer. Let's implement the remaining functionality: pagination and the ability to load more elements of a certain category.
Homeresenter
class HomePresenter extends MviBasePresenter {
private final HomeFeedLoader feedLoader;
@Override
protected void bindIntents() {
Observable loadFirstPage = ... ;
Observable pullToRefresh = ... ;
Observable nextPage =
intent(HomeView::loadNextPageIntent)
.flatMap(ignored -> feedLoader.loadNextPage()
.map(items -> new PartialState.NextPageLoaded(items))
.startWith(new PartialState.NextPageLoading())
.onErrorReturn(PartialState.NexPageLoadingError::new));
Observable loadMoreFromCategory =
intent(HomeView::loadAllProductsFromCategoryIntent)
.flatMap(categoryName -> feedLoader.loadProductsOfCategory(categoryName)
.map( products -> new PartialState.ProductsOfCategoryLoaded(categoryName, products))
.startWith(new PartialState.ProductsOfCategoryLoading(categoryName))
.onErrorReturn(error -> new PartialState.ProductsOfCategoryError(categoryName, error)));
Observable allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage, loadMoreFromCategory);
HomeViewState initialState = ... ;
Observable stateObservable = allIntents.scan(initialState, this::viewStateReducer)
subscribeViewState(stateObservable, HomeView::render);
}
private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
if (changes instanceof PartialState.NextPageLoading) {
return previousState.builder().nextPageLoading(true).nextPageError(null).build();
}
if (changes instanceof PartialState.NexPageLoadingError)
return previousState.builder()
.nextPageLoading(false)
.nextPageError(((PartialState.NexPageLoadingError) changes).getError())
.build();
if (changes instanceof PartialState.NextPageLoaded) {
List data = new ArrayList<>();
data.addAll(previousState.getData());
data.addAll(((PartialState.NextPageLoaded) changes).getData());
return previousState.builder().nextPageLoading(false).nextPageError(null).data(data).build();
}
if (changes instanceof PartialState.ProductsOfCategoryLoading) {
int indexLoadMoreItem = findAdditionalItems(categoryName, previousState.getData());
AdditionalItemsLoadable ail = (AdditionalItemsLoadable) previousState.getData().get(indexLoadMoreItem);
AdditionalItemsLoadable itemsThatIndicatesError = ail.builder()
.loading(true).error(null).build();
List data = new ArrayList<>();
data.addAll(previousState.getData());
data.set(indexLoadMoreItem, itemsThatIndicatesError);
return previousState.builder().data(data).build();
}
if (changes instanceof PartialState.ProductsOfCategoryLoadingError) {
int indexLoadMoreItem = findAdditionalItems(categoryName, previousState.getData());
AdditionalItemsLoadable ail = (AdditionalItemsLoadable) previousState.getData().get(indexLoadMoreItem);
AdditionalItemsLoadable itemsThatIndicatesError = ail.builder().loading(false).error( ((ProductsOfCategoryLoadingError)changes).getError()).build();
List data = new ArrayList<>();
data.addAll(previousState.getData());
data.set(indexLoadMoreItem, itemsThatIndicatesError);
return previousState.builder().data(data).build();
}
if (changes instanceof PartialState.ProductsOfCategoryLoaded) {
String categoryName = (ProductsOfCategoryLoaded) changes.getCategoryName();
int indexLoadMoreItem = findAdditionalItems(categoryName, previousState.getData());
int indexOfSectionHeader = findSectionHeader(categoryName, previousState.getData());
List data = new ArrayList<>();
data.addAll(previousState.getData());
removeItems(data, indexOfSectionHeader, indexLoadMoreItem);
// Добавляем все элементы категории (включая ранее удаленные)
data.addAll(indexOfSectionHeader + 1,((ProductsOfCategoryLoaded) changes).getData());
return previousState.builder().data(data).build();
}
throw new IllegalStateException("Don't know how to reduce the partial state " + changes);
}
}
The implementation of pagination (loading the next “page” with elements) is quite similar to pull-to-refresh except that we add the loaded elements to the end of the list, instead of adding them to the beginning (as we do with pull-to-refresh ) More interesting is how we load more elements of a certain category. To display the download indicator or the redo / error button for the selected category, we just need to find the corresponding AdditionalItemsLoadable object in the list of all FeedItem. Then we change this item to display the download progress bar or redo / error button. If we successfully loaded all the elements in a certain category, we look for SectionHeader and AdditionalItemsLoadable, and then replace all the elements between them with the new loaded elements.
Conclusion
The purpose of this article was to show how State Reducer can help us design complex screens with small, clear code. Just take a step back and think about how you would implement this with “traditional” MVP or MVVM without State Reducer? The key point that allows us to use State Reducer is that we have a model that reflects state. Therefore, it was very important to understand from the first part of this series of articles what the Model is. Also, State Reducer can be used only if we can be sure that the State (or rather, the Model) comes from a single source. Therefore, unidirectional data flow is also very important. I think it’s now clear why we focused on these topics in the first and second parts of this series of articles, and I hope you have the same “aha!” moment, when all the points join together. If not, don’t worry, for me it took a lot of time (and a lot of practice, and a lot of mistakes and repetitions).
Perhaps you are wondering why we did not use State Reducer on the search screen (in the second part). Using State Reducer basically makes sense when we are somehow dependent on the previous state.
Last but not least, what I want to dwell on is if you have not noticed (without going into details) all of our data is immutable (we always create a new HomeViewState, we never call the setter method on any of the objects). Therefore, you should not have problems with multithreading. The user can pull-to-refresh and at the same time load a new page and load more elements of a certain category, because State Reducer is able to produce the correct state regardless of the order of the http responses. In addition, we wrote our code with simple functions, without side effects . This makes our code super-testable, reproducible, highly parallelizable and easy to discuss.
Of course, State Reducer was not invented for MVI. You can find State Reducer concepts in many libraries, frameworks, and systems in various programming languages. State Reducer integrates perfectly with the Model-View-Intent philosophy with a unidirectional data stream and a State-reflective Model.
In the next part, we will focus on how to create reusable and reactive UI components with MVI.