The Basics of Model-View-Intent
While you have been working in Android, Web development has continued its own innovations. Redux has popularized a new approach to Web app development, and Redux in turn has led to interest in Model-View-Intent (MVI) as a GUI architecture.
However, as with the rest of the major GUI architectures, MVI is defined fairly loosely. However, at its core, it is a return to the unidirectional data flow that MVC offered… though with a few more parts:
![Model-View-Intent](mvi-intro-4.png)
Things start off a bit like MVC, where the view lets the controller know about actions that the user has taken, such as submitting a form or requesting a search. And, as with MVC, the controller is responsible for updating the model.
However, at that point, things start to differ, introducing a couple of new concepts: the view state and the reducer.
What’s a View State?
The view state is somewhat reminiscent of the view-model in MVVM. It is the data necessary to render the view, by populating widgets and so forth.
The key behind many MVI implementations is that the view state is immutable. The view is handed a view state and needs to update the UI to reflect that new state. In many cases, that is simply filling in all of the widgets. In a few cases, that might get more elaborate, such as using DiffUtil
to update the contents of a RecyclerView
.
The Redux folks would phrase this something like “the view is a function applied to the state”. The view layer does not care why the view state changed, just that it changed, and so it just updates the UI to match that state.
What’s a Reducer?
The view state may be very complex, as it needs to be as complex as the UI that is being rendered. A single activity or fragment that has multiple tabs in a ViewPager
might have several lists of material to go into those tabs, plus perhaps some additional data, all as part of the view state.
However, any individual action by the user is likely to only change a little bit of that view state. The user might mark some list item as a favorite, or add a new item, or swipe away an existing item. Most of the view state is stable.
In MVI, the controller is not directly responsible for maintaining that view state. It simply consumes actions from the view, updates the model, and publishes some sort of result to indicate that the work has been completed. Results do not have to map 1:1 to actions, though in many cases they will.
The “reducer” — whose name stems from the MapReduce model, presumably — is responsible for taking the result and crafting a new view state that reflects that incremental change in the data needed by the view. So, for example, if the user marks an item in the list as a favorite:
- The view emits an “mark-as-favorite” action
- The controller tells the model to persist that change and emits a “marked-as-favorite” result
- The reducer uses the result to update the in-memory representation of the list data to show that the favorite has been marked
The view does not update itself based upon the user’s request. Instead, it emits the action, then renders the updated view state once it arrives.
Where Does the “Intent” Thing Show Up?
In the diagram shown above, there is a split between a result and a view state. In some MVI implementations, there is also a split between an “intent” and an “action”. The intent is what the view publishes, but what the controller consumes is an action. There is a separate component that converts intents into actions.
![Model-View-Intent, With Actual Intents](mvi-intro-5.png)
Part of the rationale here is having a strictly layered separation of concerns:
- Intents and view state on the view side of the interpretor and reducer
- Actions and results on the controller side of the interpretor and reducer
For some UIs, it may be that the distinction between intents and actions may be fruitful over time. For example, perhaps you have a UI with a search option. On mobile devices, search is triggered by the typical sort of magnifying-glass icon in a toolbar. When the search is submitted, the UI wants to get a view state with search results. However, on Chromebooks or other devices with physical keyboards, you want to offer a direct typing approach, where the user can just start typing a search expression, and that automatically displays the SearchView
, skipping the toolbar icon. The result is the same: conduct a search. But, perhaps you want to distinguish those as separate intents, with an eye towards perhaps offering different behaviors for searches triggered by each mechanism. However, the controller does not care whether the search was triggered by a toolbar icon click, just typing on the keyboard, selecting some saved search in a list or whatever. The controller just needs to know that a search is required. In this case, the view can publish different intents based upon how the search was requested, but the interpretor can normalize those into a smaller set of actions.
In practice, though, this approach can wind up with a lot of code duplication, as you mind-numbingly convert intents into actions on a 1:1 basis. For the sample MVI app profiled in the next chapter, the author originally wrote the app with intent/action separation… then got rid of the intents. If you feel that the intent/action separation is worthwhile, certainly use it. In the author’s opinion, for many apps, the YAGNI principle applies: you aren’t going to need it.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.