Model-view-intent is the latest android design mode. It was inspired by Andre Staltz’s Cycle.js and adapted to the Android world by Hannes Dorfmann.

This article describes the MVI design pattern and shows you how to create a Basic Hello World application using the MVI design pattern. You may ask, why use this pattern on an application that doesn’t make any sense? There are other complex MVI examples (in the recommended reading at the end of this article) that we can all learn different things from. For me personally, a simple application like Hello World makes me more enlightened when I’m exploring something new. I hope this article will help you master the basics of MVI, and I expect you to be able to use it in complex applications.

The Kotlin source for this article is on Github.

Use Mosby to build the MVI

We will use the Mosby library to build the MVI. This library lets us focus on the big picture of programming, such as the content and business logic of MVI, rather than dealing with tricky RxJava apis and memory management. With Mosby you don’t need to care about, or write, VIew and Presenter rotation and memory leak prevention code.

Model-View-Intent

You’ve probably seen Model in other design patterns like MVC,MVP, or MVVP. But MVI’s Model is completely different from other design patterns:

  • Model represents a state (the display of data, the visibility or hiding of your controls, the sliding position of RecyclerView, etc.). Models are more formal in MVI than other design patterns. A page in your application may contain one or more Model objects. Models are defined and managed in a Domain layer. It is important to keep this constant — each request result binds a new Model instance. This ensures predictable results and testability.
  • ViewRepresents an interface and an observable that defines a set of user actionsrender(ViewState)methods
  • Intentnotandroid.content.Intent! thisIntentSimply speaking, it is an intention, or an action, or a command generated by the interaction between the user and the APP. For each user action (intent) is distributed by the View, byPresenterObservation (yes, MVI does tooPresenter).

This picture records the response of this pattern, the loop, the direction of the data flow. Our Model/State is managed by the Domain layer (with only one source) and is used to react to user actions. In any case, the new Model is created, then the corresponding View is updated, if want to get more details, I recommend a series of articles to help you start using hannesdorfmann.com/android/mos MVI mode…

Let’s have fun writing code

In our case, our UI has a button that when clicked produces a Loading indicator, followed by “Hello World” in one of the four languages.

Here’s our file structure. It’s not scary, is it? (funny.png) After all, this is a relatively simple description of the MVI pattern.

mosbymvi
  MainActivity
  HelloWorldView
  HelloWorldPresenter
mosbymvi.domain
  GetHelloWorldTextUseCase
  HelloWorldViewState
mosbymvi.data
  HelloWorldRepository
Copy the code

I chose to use the following dependencies:

dependencies {
    // Mosby
    implementation "com.hannesdorfmann.mosby3:mvi:$mosbyVersion"

    // RxBinding
    implementation "com.jakewharton.rxbinding2:rxbinding-kotlin:$rxBindingVersion"
    implementation "com.jakewharton.rxbinding2:rxbinding-support-v4-kotlin:$rxBindingVersion"
    implementation "com.jakewharton.rxbinding2:rxbinding-appcompat-v7-kotlin:$rxBindingVersion"

     // RxJava and RxAndroid
     implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
     implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
}
Copy the code

View

HelloWorldView is our View interface, which defines two methods – all of which we need to show in this example:

  1. sayHelloWorldIntent()Will trigger our intent/Action/command to display the HelloWorld text.
  2. render(state:HelloWorldViewState)Will be used to render the latest state
interface HelloWorldView : MvpView {
    /** * Emits button clicks as Observables */
    fun sayHelloWorldIntent(a): Observable<Unit>

    /** * Render the state in the UI */
    fun render(state: HelloWorldViewState)
}
Copy the code

For simplicity, we implement the View interface above in our Activity:

class MainActivity : MviActivity<HelloWorldView, HelloWorldPresenter>(), HelloWorldView {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun createPresenter(a) = HelloWorldPresenter()

    override fun sayHelloWorldIntent(a) = helloWorldButton.clicks()

    override fun render(state: HelloWorldViewState) {
        when(state) {
            is HelloWorldViewState.LoadingState -> renderLoadingState()
            is HelloWorldViewState.DataState -> renderDataState(state)
            is HelloWorldViewState.ErrorState -> renderErrorState(state)
        }
    }

    private fun renderLoadingState(a) {
        loadingIndicator.visibility = View.VISIBLE
        helloWorldTextview.visibility = View.GONE
    }

    private fun renderDataState(dataState: HelloWorldViewState.DataState){ loadingIndicator.visibility = View.GONE helloWorldTextview.apply { visibility = View.VISIBLE text = dataState.greeting }}private fun renderErrorState(errorState: HelloWorldViewState.ErrorState) {
        loadingIndicator.visibility = View.GONE
        helloWorldTextview.visibility = View.GONE
        Toast.makeText(this."error ${errorState.error}", Toast.LENGTH_LONG).show()
    }
}
Copy the code

So, let’s look at the sayHelloWorldIntent() method

 override fun sayHelloWorldIntent(a) = sayHelloWorldButton.clicks()
Copy the code

This button click is converted to Observale

so that our Presenter can observe and react. We use Jake Wharton’s RxBinding.

Note that the render(state: HelloWorldViewState) method is called by our Presenter to render the latest UI.

Which of renderLoadingState(),renderDataState(), or renderErrorState() to call depends largely on ViewState. Each function is a self-contained block of code that changes the UI to show the latest state. You don’t need to make multiple calls from Presenter to View to reflect the current state.

override fun render(state: HelloWorldViewState) {
        when(state) {
            is HelloWorldViewState.LoadingState -> renderLoadingState()
            is HelloWorldViewState.DataState -> renderDataState(state)
            is HelloWorldViewState.ErrorState -> renderErrorState(state)
        }
    }
Copy the code

Presenter

This is where the Mosby library shines! Much of the code in Presenter has already been integrated for us. All we have to do is

A). Define how to handle presenters by business logic

B). Subscribe View to ViewState observation stream, we can render different UI states according to the latest View state:

class HelloWorldPresenter : MviBasePresenter<HelloWorldView, HelloWorldViewState>() {
    override fun bindIntents(a) {
        val helloWorldState: Observable<HelloWorldViewState> = intent(HelloWorldView::sayHelloWorldIntent)
                .subscribeOn(Schedulers.io())
                .debounce(400, TimeUnit.MILLISECONDS)
                .switchMap { GetHelloWorldTextUseCase.getHelloWorldText() }
                .doOnNext { Timber.d("Received new state: " + it) }
                .observeOn(AndroidSchedulers.mainThread())

        subscribeViewState(helloWorldState, HelloWorldView::render)
    }
}
Copy the code

We use the bindIntents() method of Mosby to:

  1. Observing UI events in MVI design mode is also called Intent/ Action/commands. Note that we are using RxJavadebounce()Action to avoid quick button click event handling.
  2. Map these intents to the Domain layer. In this case,sayHelloWorldIntent()Will beGetHelloWorldTextUseCase.getHelloWorldText()The call. Internal,Mosby usedPublishSubjectTo handle intents and place memory leaks.
  3. Render the launch state (load state, data return state or error state) to the UI. Communicate with View,subscribeViewState()withBehaviorSubjectTo emit the most recently observed ViewState, as well as subsequent view states. It can keep the View up to date on its status, even as the device rotates.

Note: because Mosby uses RxJava internally, we don’t need to make clear() ViewState Disposables here -Mosby can automatically Disposables when the view is separated.

Note: IN this small example I’m not using the State reducers in the core of cycle.js /Redux/MVI. Feedback multiple user Intents (Intents) for the system to generate the current state. We can do this using the merge() and scan() operations in RxJava. You can learn more about Reducing states here

Domain layer

The Domain layer contains cases/Interactors generated based on various View states generated by user actions.

View State

Kotlin’s closed classes are great for ViewState, but you can generate ViewState in different ways.

package com.jshvarts.mosbymvi.domain

sealed class HelloWorldViewState {
    object LoadingState : HelloWorldViewState()
    data class DataState(val greeting: String) : HelloWorldViewState()
    data class ErrorState(val error: Throwable) : HelloWorldViewState()
}
Copy the code

Use Case/Interactor

In a nutshell, let’s use a unique GetHelloWorldTextUseCase. In production, you need to inject cases into your Presenter layer — read Clean Architecture for more details.

The Domain/ business logic layer is important for MVP and MVVM, but for MVI, it goes a step further. This is where we define and generate the different Models/View states. Models are immutable — each request for business logic generates one or more new Model objects, which can render the UI.

/** * In a Production app, inject your Use Case into your Presenter instead. */
object GetHelloWorldTextUseCase {
    fun getHelloWorldText(a): Observable<HelloWorldViewState> {
        return HelloWorldRepository.loadHelloWorldText()
                .map<HelloWorldViewState> { HelloWorldViewState.DataState(it) }
                .startWith(HelloWorldViewState.LoadingState)
                .onErrorReturn { HelloWorldViewState.ErrorState(it) }
    }
}
Copy the code

Let’s go line by line gethelloWorldTextusase #getHelloWorldText()

// Call the repository to get the data
HelloWorldRepository.loadHelloWorldText()
// Create DataState and convert it to HelloWorldViewState
.map<HelloWorldViewState> { HelloWorldViewState.DataState(it) }
// Emit LoadingState before transmitting data
.startWith(HelloWorldViewState.LoadingState())
// Do not throw an error - emit an error state instead
.onErrorReturn { HelloWorldViewState.ErrorState(it) }
Copy the code

We added the log function to Presenter’s doOnNext() function, and when the button is clicked, if we can see the following then we are successful

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

If an error (IllegalStateException) occurs, the Log will contain the following:

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

Data layer

To put it simply, our Data layer contains only a subclass, HelloWorldRepository, that generates an observable flow that randomly generates the HelloWorld each time it is called.

object HelloWorldRepository {

    fun loadHelloWorldText(a): Observable<String> = Observable.just(getRandomMessage())

    private fun getRandomMessage(a): String {
        val messages = listOf("Hello World"."Hola Mundo"."Hallo Welt"."Bonjour le monde")
        return messages[Random().nextInt(messages.size)]
    }
}
Copy the code

Recommended reading (Articles are almost always over the wall)

  • Source code for this article
  • MVI distribution explanation series
  • Cycle.js
  • Mosby library github.com/sockeqwe/mo…
  • Managing state with RxJava
  • MVI on Android