Model-View-Intent is the newest design pattern on Android. It was inspired by Cycle.js by André Staltz and adopted to Android world by Hannes Dorfmann.

This article introduces MVI pattern and shows you how to build a basic Hello World app using MVI. You may ask, why apply an architectural pattern to such a simple app with no real-world application? There are a couple of other more complex MVI examples out there (see the Recommended Reading section below) an we all learn differently. For me personally seeing a basic example like this is more likely to give me that aha moment when exploring something new. I hope that this article will help you grasp the basics of MVI and inspire you to dive deeper and apply the pattern in more complex real-world apps.

Kotlin source code for this article is on Github.

MVI with Mosby

We’ll use MVI library from Mosby. This lets us concentrate on a big picture such as MVI concepts and business logic rather than dealing with non-trivial RxJava API and memory management. With Mosby you don’t have to worry about wiring your View and Presenter to handle rotation and prevent memory leaks.

Model-View-Intent

You’ve seen Model and View before in other patterns such as MVC, MVP and MVVP. But the reactive nature of MVI makes things quite different:

Model represents a state (data to be displayed, visibility of your View widgets, RecyclerView scroll position, etc.). Model is more formalized in MVI than in other patterns. A screen of your app may be composed of 1 or more Model objects. Model is defined and managed in one place only, the Domain layer. It is important to make it immutable — every request results in brand new Model instance. This assures predictable results and testability.

represents a state (data to be displayed, visibility of your View widgets, RecyclerView scroll position, etc.). Model is more formalized in MVI than in other patterns. A screen of your app may be composed of 1 or more Model objects. Model is defined and managed in one place only, the Domain layer. It is important to make it immutable — every request results in brand new Model instance. This assures predictable results and testability. View is represented by an interface which defines a set of user actions (intents) as Observables and a render(ViewState) method.

is represented by an interface which defines a set of user actions (intents) as Observables and a method. Intent is not android.content.Intent ! The intent simply describes an intention, or action, or command generated by the user as he or she is interacting with the app. For each user action an intent is dispatched by the View and observed by the Presenter (yes, MVI has a Presenter too).

Note the reactive, cyclic, unidirectional flow of data of this pattern. Our Model/State is managed by the Domain layer (Single Source of Truth) in response to user actions. Whenever new Model is created, the View is updated. For more details, I recommend multi-part series on MVI starting at http://hannesdorfmann.com/android/mosby3-mvi-1

Let’s see the code

In our simple app the UI has one button which triggers a loading indicator followed by displaying “Hello World” in 1 of 4 different languages.

Here is our package structure. It’s not intimidating, is it? After all, it is a gentle introduction to MVI.

mosbymvi

MainActivity

HelloWorldView

HelloWorldPresenter mosbymvi.domain

GetHelloWorldTextUseCase

HelloWorldViewState mosbymvi.data

HelloWorldRepository

I chose to use the following libraries:

Snippet of build.gradle

View

As in MVP, our View implementation is based on an interface (contract). This way, if the View implementation changes, the behavior will stay the same so the Presenter does not need to change. We make our View as dumb and passive as possible which reduces the behavior of the View to the absolute minimum. These View design decisions dramatically improve testability.

HelloWorldView is our View interface which defines two methods — all that’s needed for our simple app:

sayHelloWorldIntent() will trigger our intent/action/command to display the Hello World text. render(state: HelloWorldViewState) will be used to render the most recent state.

For this example, HelloWorldView is implemented by an Activity:

Take a look at the sayHelloWorldIntent() method above: the button clicks are converted into Observable<Unit> stream so that our Presenter can observe and react to it. We use RxBinding library by Jake Wharton to do it.

Also note the render(state: HelloWorldViewState) method which will be invoked to render the latest Model in the UI.

Depending on the ViewState instance sent to the View, a corresponding render function will be invoked: renderLoadingState() , renderDataState(), renderErrorState() . Each is a self-contained block of code that will change the UI to reflect the latest state. No need for multiple calls from the Presenter to the View to reflect the current state.

Using Kotlin’s sealed classes in combination with the when expression gives us an extra benefit of ensuring that all possible states are accounted for. For instance, if we omit one of the states (e.g. HelloWorldViewState.LoadingState ), the code simply won’t compile. This makes sealed classes in Kotlin a great candidate for representing different Models/states in MVI.

Presenter

This is where Mosby really shines! A lot of the code in the Presenter is done for us under the hood. All we have to do is a) define how intents are handled by the business logic and b) subscribe the View to the ViewState Observable stream so we can render different UI states based on the latest ViewState.

We use Mosby’s bindIntents() method to:

Observe UI events also known as intents/actions/commands in MVI. Note that we use the debounce() operator from RxJava is to avoid handling button clicks in rapid succession. Map those intents into calls to the Domain layer. In this case, sayHelloWorldIntent() will be calling GetHelloWorldTextUseCase.getHelloWorldText() . Internally, Mosby uses PublishSubject to process intents while preventing memory leaks. Render each state emitted (Loading State, Data Returned State or Error State) into the UI. To communicate with the View, subscribeViewState() uses BehaviorSubject which emits the most recent ViewState it has observed as well as all the subsequent ones. This allows the View to always know the latest state even after device rotation.

Note the unidirectional flow of the Presenter code: we do not use any side-effects in the RxJava chain (aside from debugging) to update the UI state from the Presenter. Instead, the View pushes user actions to the Presenter which makes calls to the Domain layer to get the new Model. Whenever the Presenter receives the new Model, the updates are pushed back to the UI.

Because Mosby uses RxJava internally, there is no need for us to clear() ViewState Disposables — Mosby does it automatically in MviBasePresenter#destroy() when the View is detached permanently.

Note that for this simple app I don’t utilize State reducers which is one of the core concepts of Cycle.js/Redux/MVI. It is an idea of feeding in several user intents to be “reduced” by the system to generate the current state. We can use a combination of RxJava’s merge() and scan() operators to achieve it. You can learn more about “reducing” state here.

Domain Layer

Domain/Business Logic layer is important in MVP and MVVM but MVI takes it a step further. This is where our immutable Models/View States for different screens are defined and generated. Every user action triggers a new request to the Domain layer which creates 1 or more Model/View State objects be rendered in the UI.

Our Domain layer consists of two classes HelloWorldViewState and GetHelloWorldTextUseCase .

View State

Kotlin’s sealed classes are perfectly suited to implement our ViewState but you could implement the View State in a variety of ways.

Basically, we just built a State Machine. Each of the possible states is mutually exclusive — you can only be in one of the states at any given time.

Use Case/Interactor

For simplicity, our Use Case is a Singleton. In a Production-quality app, you’d want to inject the Use Case into your Presenter — for more details read about Clean Architecture here.

Let’s look at GetHelloWorldTextUseCase#getHelloWorldText() line by line:

// Make a call to the repository to get the data

HelloWorldRepository.loadHelloWorldText() // Create DataState and cast it into HelloWorldViewState

.map<HelloWorldViewState> { HelloWorldViewState.DataState(it) } // Emit LoadingState value prior to emitting the data

.startWith(HelloWorldViewState.LoadingState()) // Do no throw an error--emit the ErrorState instead

.onErrorReturn { HelloWorldViewState.ErrorState(it) }

Since we added logging in doOnNext() in the Presenter, when the button is clicked, we see the following in the log in case of success (note that the LoadingState is always emitted first):

Received new state: HelloWorldViewState$LoadingState Received new state: DataState(greeting=Hello World)

In the case of an error (say IllegalStateException), the log will contain the following (again, note that the LoadingState is emitted first):

Received new state: HelloWorldViewState$LoadingState Received new state: ErrorState(error=java.lang.IllegalArgumentException)

Data layer

For simplicity, our Data layer consists of only one Singleton class, HelloWorldRepository which generates an Observable stream of a random Hello World messages after each call.

Recommended Reading