DEV Community

Cover image for Implement MVI pattern with just few step
Randy Arba
Randy Arba

Posted on

Implement MVI pattern with just few step

I just expirement with MVI pattern last week ago, when i tried to implement that i found some confusion cos it different from the other pattern that i have done before. First some developer are face the complex and more use case every they develop program. Because of that i try to search the solution to solve that problem we create some pattern based on MVI to facing that use case.

I try to create the sample with login and register flow using MVI pattern design. you can fork or clone in this url but it still under development process. First we are gonna decomposition about the packaging, there are 4 package in Module package domain, app, and entities. Each package contain different purpose. Domain layer contain executing business logic independent without android dependecies. App will contain about the android activity and implement android dependecies. Then Entities is about related business rules that critical function in application, usually contain data class or POJO.

Kotlin Give you Everything

When we develop Android with reactive programming style in MVVM or MVI we used some library like Rxjava, Live data, outside of Android default dependencies. Rxjava is powerfull enough for boosting our Android development and we still use it, but we focus in Kotlin language, and what Kotlin can provide for us. first we can compare the function that sama behaviour in Kotlin feature. There are contain function that same as Live data or Rxjava Behavaiour a.k.a Channels, Channels is one of Courotine feature that still Beta/Experimental version, but you can still use it for experiment. You can see the below code and you try in your IDE.

suspend fun channelsExample() {

        val numbersStream: Channel<Integer> = Channel(Channel.CONFLATED)


        for (value in numbersStream) {
            println("value updated :")
            println(value)
        }

        numbersStream.offer(1)
        numbersStream.offer(2)
        numbersStream.offer(3)

        val lastNumber = numbersStream.poll()

        println(lastNumber)
    }

Based on that sample the purpose is sample handle everytime object change when sending and subscribing. The different thing is Channel using suspend keyword, yeah the if you using Courotine before that keyword is not strange anymore. Suspend is like kotlin model concurrency language level, for send or listen every object change to a Channel. If you try without suspend keyword it will error occure why? cos we are actually doing blocking operation if we using the main thread. I must warn we know the Channel is same concept like Live data or Behaviout subject but we need ConflatedBroadcastChannel. ConflatedBroadcastChannel is only the the most recently sent value is received, while previously sent elements are lost.

Ready for the Pattern
If you search about MVI pattern in Android you should see the Hannes Dorfmann result. He already implement the pattern before and make suitable for Android development process the result is he create library that known as Mosby. I tried before and its awesome but for know about the basic concept of MVI pattern Mosby not suitable enough.

1- First Declare the UI Model

UI model is data that be displayed in the UI screen its seem like data class in kotlin. Its different thing from Android View Model class. First you can see the the Model below. I tried to create Login model, the sample below can known as View State.

data class Model(
    val error: String? = null,
    val progress: Boolean = false,
    val authenticationResponse: AuthenticationResponse? = null
)

As you can see that we create login model that have progress bar, error, and response state. We define that based on Login screen, in login screen there are progress bar, view for showing error, then two Edit text for username and password (it hold on AuthencationResponse class) and two button for register and login.

In MVI everytime update occure, the android will draw all the UI Element based on Model class. We limit the model that we don not need other data from outer to draw the UI.

2- Initiate the Intents/Action

First you must recognize the intents in Android development but its wrong. Intents is event or action that will occure in android screen. example when we create the login screen there are action login and register to be handle. When we declare the intent usually we using data class inside sealed class. Sealed class is like ENUM but with powerfull function cos is not only String object can defined

sealed class Intents
data class RequestLogin(val userName: String, val password: String) : Intents()
data class RequestRegister(val userName: String, val password: String) : Intents()

3- Initiate the Channels and Logic

We will create Channel that contain or hold the Intents(Action) and hold the UI models. ViewModel will start listening from Intents Channel checking there are data is changed or update or not. After that based on action the data will displayed on UI screen. We use Rxjava Single, and Courotine scope. *Note if you not familiar with some keyword you can comment below.

fun LoginFragment.model(channels: Pair<Channel<Intents>, SendChannel<UiModel>>) {
    lifecycleScope.handleIntentsAndUpdateUiModels(channels.first, channels.second)
}

private fun CoroutineScope.handleIntentsAndUpdateUiModels(
    intents: Channel<Intents>,
    uiModels: SendChannel<UiModel>,
    useCase: LoginUseCases = LoginUseCases(intents, uiModels)
) = launch(SupervisorJob() + Dispatchers.IO) {
    uiModels.send(UiModel())
    for(intent in intents) {
        when (intent) {
            is RequestLogin -> useCase.login(intent)
            is RequestRegister -> useCase.register(intent)
        }
    }
}

private data class LoginUseCases(
    val intents: Channel<Intents>,
    val uiModels: SendChannel<UiModel>,
    val loginUseCase: suspend (String?, String?) -> AuthenticationResponse = loginRequest,
    val registerUseCase: suspend (String?, String?) -> AuthenticationResponse = registerRequest,
    val login: suspend (RequestLogin) -> Unit = loginRequester(uiModels, loginUseCase),
    val register: suspend (RequestRegister) -> Unit = registerRequester(uiModels, registerUseCase)
)

private fun registerRequester(
    view: SendChannel<UiModel>,
    registerUseCase: suspend (String?, String?) -> AuthenticationResponse
) : suspend (RequestRegister) -> Unit = {
    view.send(UiModel(progress = true))
    val response = registerUseCase(it.userName, it.password)
    view.send(UiModel(response.errorMessage,false, response))
}

private fun loginRequester(
    view: SendChannel<UiModel>,
    loginUseCase: suspend (String?, String?) -> AuthenticationResponse
) : suspend (RequestLogin) -> Unit = {
    view.send(UiModel(progress = true))
    val response = loginUseCase(it.userName, it.password)
    view.send(UiModel(response.errorMessage, false, response))
}



private val loginRequest: suspend (String?, String?) -> AuthenticationResponse = {
    userName, password -> requestLogin(checkNotNull(userName), checkNotNull(password)).blockingGet()
}

private val registerRequest: suspend (String?, String?) -> AuthenticationResponse = {
    userName, password -> requestRegister(checkNotNull(userName), checkNotNull(password)).blockingGet()
}

Nah after that we automatically handled the relation between Intents Channel and UI Model Channel that we create before. The next task is to do draw the UI from UI MOdels channel and update intenst Channel based on the action.

4. Connect Ui Model

Last but not least this is the next step after we initate the channels. We must initialize current events cycle that updates the Intents Channel. Model will listen each Channel update, and present them into the UI, when we talk about the UI we talk about the activity or Fragment so we implement that in onCreate() or onViewCreate() method. We separate it and create in View class, we just create with Kotlin extension function. this will help you calling the model and make the activity class look simple.

fun LoginFragment.view(intents: Channel<Intents>): Pair<Channel<Intents>, SendChannel<UiModel>> {
    return intents to lifecycleScope.actor {
        for(model in channel) {
            updateProgress(model)
            updateLoginButton(model, intents)
            updateRegisterButton(model, intents)
            updateErrorTextView(model)
            updateNavigation(model)
        }
    }
}

private fun LoginFragment.updateNavigation(uiModel: UiModel) {
    uiModel.authenticationResponse
        ?.takeIf { it.success }
        ?.also {
            activity?.finish()
        }
}

private fun LoginFragment.updateErrorTextView(uiModel: UiModel) {
    errorTextView.text = uiModel.error ?: ""
}

private fun LoginFragment.updateProgress(uiModel: UiModel) {
    progressBar.visibility = if(uiModel.progress) View.VISIBLE else View.GONE
}

private fun LoginFragment.updateLoginButton(uiModel: UiModel, intents: SendChannel<Intents>) {
    if(uiModel.progress) loginButton.setOnClickListener(null)
    else loginButton.setOnClickListener {
        lifecycleScope.launch(SupervisorJob()  + Dispatchers.IO) {
            intents.send(
                RequestLogin(
                    userNameEditText.text.toString(),
                    passwordEditText.text.toString()
                )
            )
        }
    }
}

private fun LoginFragment.updateRegisterButton(uiModel: UiModel, intents: SendChannel<Intents>) {
    if (uiModel.progress) registerButton.setOnClickListener(null) else {
        lifecycleScope.launch(SupervisorJob() + Dispatchers.IO) {
            intents.send(
                RequestRegister(
                    userNameEditText.text.toString(),
                    passwordEditText.text.toString()
                )
            )
        }
    }
}

We call the method with courotine in Main Thread (Intentes Channel will send the new Action) that to start listening the UI Models Channel if there any update or not.
MVI Pattern is like event based cycle, View will listening the UI models updates, if there any action trigger update the UI model will automatically update the UI. Nah After that we just call this simple code in activity or fragment.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model(view(intents()))
    }

Note, i must thanks to Ahmed Adel ismail that inspire me to write this cos it based his Repo and article and i tried to implement and experiment it, anyway i using Hannes Dorfmann pattern to with mosby library. Btw sorry for my english.

Top comments (0)