The Contract of the Model-View-Intent Architecture
There are multiple articles, talks, and podcasts that address the topic of what the Model-View-Intent architecture is, but I rarely hear about what I think are the principles of this architecture.
Model-View-Intent in a few words
The MVI architecture is a pattern which aims at organizing the higher layers, i.e. close to the UI, of your application to make its development simple. Now, the architecture is reactive and since most of us, Android developers are not used to the reactive paradigm, I believe the simplicity of the MVI architecture remains overlooked.
As I see it, the simplicity is found in a contract which the MVI architecture defines that the developer needs to respect. I’d like to share what this contract is.
User Interface
The UI (or the View) is separated into two distinct components: the listening and the rendering. The job of the listening part is to listen to the user and emit any user’s originated intent (e.g., click, scroll, etc.) for the ViewModel to listen. The job of the rendering part is to render a state onto the screen (e.g., display lists, switch loading/empty views, etc.). Here is the UI’s contract:
The “listening” and the “rendering” cannot talk to each other
In Android, inputs and outputs are at the same place. This is true for both the hardware (the screen) and the software (activity/fragment). I think it helps one’s mind to keep them both isolated from each other. The listening only listens to the user. The rendering only listens to emitted states from the ViewModel.
The “listening” cannot write anything to the UI
It can only read it. If the user clicks on a refresh button, the code should not display a loading view. It only needs to emit an intent which conveys that the refresh button has been clicked. That’s it. If extra information is needed, the listening may read the UI (e.g. which row has been clicked, what is the value of the input, etc.) but should never change anything.
The “rendering” cannot read anything from the UI
It can only write it. It means that the state which is going to be rendered, is self-sufficient. It should contain every data required to render the screen.
One Entry Point, One Exit Point
From the point of view of a MVI architecture, the UI has two, and only two methods:
interface MviView {
Observable<MviIntent> intents();
void render(MviViewState state);
}
It emits intents for the ViewModel to listen to, this it the job of the listening. It also takes a state to render a screen, this is the job of the rendering. The UI has only one entry point, and it has only one exit point.
ViewModel
UI Independent Logic
In Android, because of configuration changes, the UI may have a shorter lifecycle than the ViewModel. The ViewModel should never be affected by the lifecycle of the UI. Even if the UI disconnects or is discarded, the ViewModel should keep its data flow alive in order to keep ongoing processes and the latest cached state safe.
One Entry Point, One Exit Point
From the point of view of a MVI architecture, the ViewModel has two, and only two methods:
interface MviViewModel {
void processIntents(Observable<MviIntent> intents);
Observable<MviViewState> states();
}
It takes an observable of intents in order to execute their corresponding business logic. This will produce a result that will be reduced with the latest cached state to create a new state that the ViewModel will emit in the data flow. The ViewModel has only one entry point, and it has only one exit point.
user(rendering(businessLogic(listening(user()))))
This is the reactiveness of the architecture and the contract between its components. This is a cycle where each component listen to the previous one, and is listened by the next. One component processes its input in some way and its output becomes the input of the next component, ad eternum.
I changed the naming because Model-View-Intent conflicts head first with the Android framework.
- Listening is the Intent in MVI
- BusinessLogic is the Model in MVI
- Rendering is the View in MVI
- User is our best friend.
Unidirectional Data Flow
The data can only be passed around via the data flow and this data flow can only go one way. Each components of the architecture only knows what kind of data they are listening to and what they’re going to do with it. The only one way for the components to communicate to each other is by passing an object onto the data flow.
Immutability
All objects passed onto the data flow are immutable, they never change. Components have to create new objects to pass new or updated data.
Side Effects
Side effects are necessary for any application to be useful. The MVI architecture isn’t an exception. But the contract sets limitation to where side effects may happen. The goal is to restrain as much as possible the scope of those side effects.
- Inside the BusinessLogic: it may need to communicate with the repository to store/load things, etc.
- Inside the Rendering: it may write the UI.
- Inside the Listening: it may read the UI.
Side effects can only happen in this 3 cases. Also important, they cannot communicate to each other. Every side effect is contained inside its “zone”.
Conclusion
The Model-View-Intent architecture is not easy to implement but I believe that if it is implemented in respect to the contract it brings, it can help make the development and maintenance of an application simple and robust. What do you think?
ps: if you’re curious about what an implementation of the MVI architecture could look like, you can check this sample project.