Getting started with MVI Architecture on Android

Getting started with MVI Architecture on Android

Introduction

Like MVC, MVP or MVVM, MVI is an architectural design pattern that helps us better organize our code to create robust and maintainable applications. It is in the same family as Flux or Redux and was first introduced by André Medeiros. This acronym is formed by the contraction of the words Model, View and Intent.

We’ll try to describe the different elements of the MVI architecture so that you can understand their role within the architecture.

Figure 1Figure 1

Intent

Represents the user’s intent when interacting with the UI. For example, a click on a button to refresh a list of data will be modeled as an Intent. To avoid any confusion with the Android framework Intent, we will call it in the rest of this article a UserIntent.

Model

It is a ViewModel where different synchronous or asynchronous tasks are performed. It accepts UserIntents as input and produces one or more successive states as output. These states are exposed via a LiveData to be used by the view.

View

The view simply processes immutable states that it receives from the ViewModel to update the UI. It also allows to transmit user actions to the ViewModel in order to accomplish defined tasks.

But it’s not finished, to set up an MVI archictecture, we need other elements like :

State

It represents an immutable state of sight. A new state is created by the ViewModel each time the view needs to be updated.

Reducer

When you want to create a new State, you use the Reducer. It is provided with the current state as well as new elements to be included and it takes care of producing an immutable state.

MVI in the nutshell

As you can see on the Figure 1, all the actions that the user can perform on a view are translated as intention, in other words the user manifests the intention to do something by transmitting his intention to the model, the model receives the intention then performs the operation related to this intention, then the model returns a result in the form of a state, and the view will be able to update itself according to this state.

MVI was designed around the paradigm of reactive programming and uses observable flows to exchange messages between different entities. As a result, each of them will be independent and therefore more flexible and resilient. In addition, information will always flow in one direction: this concept is known as Unidirectional Data Flow or UDF. Once the architecture is established, the developer will find it easier to reason and debug if necessary. However, this concept must be strictly adhered to throughout the development process.

The MVI components

To illustrate the MVI architecture, we’re going to develop step by step an application that retrieves the data online and then displays them in a recyclerView, the user will be able to refresh the list to get the new data, the application looks like that

The UI state

Extremely important thing with MVI, we model a complete state of the view with all the data necessary to display our UI

And the state of figure 2 can be presented by this class

As you can see, the State class implements the IState interface, it’s just a matter of organization, and reusability you’ll see the advantage in the rest of the article.

The View

The View part is represented in our Android application by an Activity. It will implement a generic interface with a single render function that takes a State state as a parameter.

The User Intent

The UserIntent class represents the different actions the user can do on a particular view, and in our case the user may want to retrieve the data or refresh the view to get the new information,

We have used a Sealed class that models these “intentions” and allows us to deal with them exhaustively in the instruction when provided by Kotlin.

sealed class UserIntent : IIntent {
    object RefreshUsers : UserIntent()
    object FetchUsers : UserIntent()
}

The UserIntent class also implements an IIntent interface that we used to identify all the Intentions that the user can perform in our application.

interface IIntent {

}

The model

Let us now turn to the most interesting part of our logic. We will try to keep in mind the two concepts mentioned above: UDF and Reactive Programming. We will only use what Kotlin, LiveData and the Coroutines library offer.

The ViewModel will implement a generic interface which exposes the state via a LiveData and which offers an entry point for UserIntents.

All the viewModels of our application will implement this interface and will receive the user’s intentions via a channel (for more information about the channel refer to the official Kotlin documentation).

MVI In Practice

The ViewModel of the application

The UserViewModel of our small application implements the generic IModel interface, and this ViewModel will be able to provide the view with a UserState state and will be able to handle UserIntent intentions, that’s why the type parameter looks like .

class UserViewModel(private val userApi: UserApi) : ViewModel(), IModel<UserState, UserIntent> {

    override val intents: Channel<UserIntent> = Channel(Channel.UNLIMITED)

    private val _state = MutableLiveData<UserState>().apply { value = UserState() }
    override val state: LiveData<UserState>
        get() = _state

    init {
        handlerIntent()
    }

    private fun handlerIntent() {
        viewModelScope.launch {
            intents.consumeAsFlow().collect { userIntent ->
                when(userIntent) {
                    UserIntent.RefreshUsers -> fetchData()
                    UserIntent.FetchUsers -> fetchData()
                }
            }
        }
    }

    private fun fetchData() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                updateState { it.copy(isLoading = true) }
                updateState { it.copy(isLoading = false, users = userApi.getUser()) }
            } catch (e: Exception) {
                updateState { it.copy(isLoading = false, errorMessage = e.message) }
            }
        }
    }

    private suspend fun updateState(handler: suspend (intent: UserState) -> UserState) {
        _state.postValue(handler(state.value!!))
    }
}

When implementing the IModel interface, the UserViewModel must implement both properties, the intents property that is initialized with UNLIMITED channel, and the state returns the value of a MutableViewModel that is initialized by the initial state of the view.

Intent handling

The handlerIntentmethod collects the intentions that the user is going to send via the channel from the view and performs the appropriate processing according to the received intention.

fun handlerIntent() {
    viewModelScope.launch {
        intents.consumeAsFlow().collect { userIntent ->
            when(userIntent) {
                UserIntent.RefreshUsers -> fetchData()
                UserIntent.FetchUsers -> fetchData()
            }
        }
    }
}

Update the state

When you want to create a new State, you use the Reducer, It is provided with the current state as well as new elements to be included and it takes care of producing an immutable state.

private suspend fun updateState(handler: suspend (intent: UserState) -> UserState) {
    _state.postValue(handler(state.value!!))
}

This operation is performed by the updateState method, which takes as only parameter a lambda that takes the current state and returns another state.

The Main View

The View part is represented in our Android application by the MainActivity. It will implement a generic interface with a single render function that takes a State state as a parameter.

class MainActivity : AppCompatActivity(), IView<UserState> {

    private val mAdapter = ItemAdapter()
    private val mViewModel by viewModel<UserViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        recyclerView.adapter = mAdapter

        // Observing the state
        mViewModel.state.observe(this, Observer {
            render(it)
        })

        // Fetching data when the application launched
        lifecycleScope.launch {
            mViewModel.intents.send(UserIntent.FetchUsers)
        }

        // Refresh data
        btnRefresh.setOnClickListener {
            lifecycleScope.launch {
                mViewModel.intents.send(UserIntent.RefreshUsers)
            }
        }
    }

    override fun render(state: UserState) {
        with(state) {
            progressBar.isVisible = isLoading
            btnRefresh.isEnabled = !isLoading
            mAdapter.submitList(users)

            if (errorMessage != null) {
                Toast.makeText(this@MainActivity, errorMessage, Toast.LENGTH_SHORT).show()
            }
        }
    }
}

When the user click the refresh button, we send the RefreshUsers intent to the UserViewModel using the channel, this operation will update the state and the main activity will react to the state change by call the render method and pass to it new state that will be used to update the UI.

Conclusion

MVI is an architectural design pattern based on reactive programming. The goal is to have less complex code, testable and easy to maintain. An Intent (or UserIntent in this article) describes the action of a user. Actions are always executed following the same one-way circuit (UDF). We manipulate immutable states that model the view. A Reducer is a component that allows us to produce new states.

If you want to see the code more closely, the source code is available on the following link and If you have any kind of feedback, feel free to connect with me on Twitter.

References

Channels Deferred values provide a convenient way to transfer a single value between coroutines. Channels provide a way to…kotlinlang.org

Créer une application Android en utilisant le pattern MVI et Kotlin Coroutines - Publicis Sapient… Avec , les développeurs Android ont à disposition des outils très puissants pour les aider à concevoir des applications…blog.engineering.publicissapient.fr

Sealed Classes Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a…kotlinlang.org

Did you find this article valuable?

Support Eric Ampire by becoming a sponsor. Any amount is appreciated!