MVI Pattern For Android In 4 Steps
https://lambda-it.ch/blog/post/reactive-data-flow-in-angular-2

MVI Pattern For Android In 4 Steps

Lately I wrote an article about MVI pattern, but as we are facing new problems every day and face more use-cases, we manage to find better ways to solve our problems, and this article is targeting Android Developers who are trying to learn about MVI pattern to be used in production

If you'd like to see the code, this link will send you to a Login/Registration screen that is fully functional with MVI pattern

Kotlin is enough

We usually used a library to supply us with a Reactive programming style, to be able to implement MVVM or MVI, but Kotlin did a great job with providing us with Coroutines ... but first we have to understand Kotlin's Channels

In short, a Channel is similar to Google's Live-Data or RxJava's Behavior-Subject, to cut it short, the following code sample shows how to create, listen and update the 3 types :

class Streams {




    fun behaviorSubjectExample() {


        // initialize
        val numbersStream: BehaviorSubject<Integer> = BehaviorSubject.create()


        // observe / listen to updates
        numbersStream.subscribe {
            System.out.println("value updated :")
            System.out.println(it)
        }


        // update
        numbersStream.onNext(1)
        numbersStream.onNext(2)
        numbersStream.onNext(3)


        // get latest value
        val lastNumber = numbersStream.value
    }



    fun liveDataExample() {


        // initialize
        val numbersStream: MutableLiveData<Integer> = MutableLiveData()


        // observe / listen to updates
        numbersStream.observeForever {
            System.out.println("value updated :")
            System.out.println(it)
        }


        // update
        numbersStream.value = 1
        numbersStream.value = 2
        numbersStream.value = 3


        // get latest value
        val lastNumber = numbersStream.value
    }



    suspend fun channelsExample() {


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


        // observe / listen to updates
        for (value in numbersStream) {
            System.out.println("value updated :")
            System.out.println(value)
        }


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


        // get latest value
        val lastNumber = numbersStream.poll()
        
    }




}

The only difference here is that Channels make use of Kotlin native features like "suspend" keyword, as Kotlin models concurrency in the language level, to send or listen to a Channel, this should be done in a "suspended" code block, and this is very valid approach since we are actually doing blocking operations

Note that to make Channels behave like BehaviorSubjects or LiveData exactly, you have to use ConflatedBroadcastChannel, but this is out of scope of this article, we do not need in MVI more than the part in the example

The Pattern In Action

Now for implementing the pattern, Although I believe that Hannes Dorfmann is one of the best Android Developers out there, but his library MOSBY is not the a good choice to start implementing MVI, I highly recommend understanding the core concepts from it's creator Andre Staltz

In this Article, I will try to explain how to implement it rather than the concepts behind it

1- Declare the UI Model

According to it's creator, the UI Model here is not our known class in Android ViewModel.java, but the UI model is the data to be displayed on the UI, in our Login Sample the UI Model (you can call it View State, or View Model) it will be just like this :

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

We have a Login screen that holds a progress bar, a view for error message, and 2 Edit texts for username and password, and 2 buttons (one for login and the other is for registration)

if our screen had to display a list of items for example, the Model class will a List of items to be displayed

Notice that in MVI, every time an update happens, we draw all the UI elements from this Model class, we do not need any other data from any where to draw the UI ... and this can be costly at some screens (we can split our screen into multiple MVIs but this is out of the scope of this article)

2- Declare Intents / Actions

Then we have to see what are the events / actions that will happen on our screen, in our login example, we have 2 buttons, login and register, so we have 2 actions to be handled, so we declare these actions (called Intents ... do not be confused with Android Intents, both are different things)

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

Making use of the Sealed classes feature in Kotlin, this will help in handling those Intents when triggered

3- Declare the Channels and Handling logic

on other platforms, this is a function, but for Android we have a rotation problem, which is out of the scope of this article, but in all cases, we have to declare our handling logic and channels in a ViewModel

we will declare a Channel that will hold the Intents / Actions

and another channel that will hold the UI Models

and in our ViewModel we will start listening to the Intents Channel, and based on the action we take the action and then update the UI Models Channels with the data to be displayed on the UI

So our ViewModel will look like this :

class LoginViewModel(
	    val intents: Channel<Intents> = Channel(Channel.CONFLATED),
	    val models: Channel<Model> = Channel(Channel.CONFLATED),
	    val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) : ViewModel() {
	

	    init {
	        coroutineScope.launch {
	            models.send(Model())
	            for (intent in intents) {
	                when (intent) {
	                    is RequestLogin -> requestLogin(intent)
	                    is RequestRegister -> requestRegister(intent)
	                }
	            }
	        }
	    }
	

	

	    private suspend fun requestLogin(intent: RequestLogin) {
	        models.send(Model(progress = true))
	        val response = loginUseCase(intent.userName, intent.password)
	        models.send(Model(response.errorMessage, false, response))
	    }
	

	

	    private suspend fun requestRegister(intent: RequestRegister) {
	        models.send(Model(progress = true))
	        val response = registerUseCase(intent.userName, intent.password)
	        models.send(Model(response.errorMessage, false, response))
	    }
	

	    public override fun onCleared() {
	        coroutineScope.cancel()
	        intents.cancel()
	        models.cancel()
	        super.onCleared()
	    }
	}
	
}


* for the loginUseCase() and registerUseCase(), consider those as 2 suspended functions that makes API calls and get back with a response

Now we have handled the relation between the Intents Channel and the UI Models Channel, all we have to do is to draw the UI from the UI Models Channel, and update Intents Channel when an action happens

4- Connect things together on the UI

In our last step, we have to initialize our events cycle, which updates the Intents Channel, receives the Models from there Channel, and View them on the UI, and this is done from our Activity's onCreate() or Fragment's onViewCreated() :

class LoginFragment : Fragment() {


    ...

    // this is considered the view() function :

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        super.onViewCreated(view, savedInstanceState)


        // with() is a scope function in kotlin, inside the coming block 
        // all members of the LoginViewModel are accessible 
        // as if this code is written inside the LoginViewModel class

        with(ViewModelProviders.of(this).get(LoginViewModel::class.java)) {


            // the lifecycleScope() is a coroutineScope supplied by kotlin for 
            // Activities and Fragments

            lifecycleScope.launch {


                // here we are listening to the UI Models 
                // so we can update the UI views on the Main thread

                for (model in models) {
                    updateProgress(model)
                    updateLoginButton(model, intents)
                    updateRegisterButton(model, intents)
                    updateErrorTextView(model)
                    updateNavigation(model)
                }


            }
        }


    }


    private fun updateNavigation(model: Model) {
        model.authenticationResponse
            ?.takeIf { it.success }
            ?.also {
                startActivity(Intent(context, HomeActivity::class.java))
                activity?.finish()
            }
    }


    private fun updateErrorTextView(model: Model) {
        errorTextView.text = model.error ?: ""
    }




    private fun updateProgress(model: Model) {
        progressBar.visibility = if (model.progress) View.VISIBLE else View.GONE
    }


    /**
     * here we need the Intents Channel to be able to send user events to it,
     * which will cause the UI Models to be updated later
     */
    private fun updateLoginButton(model: Model, intents: Channel<Intents>) {
        if (model.progress) loginButton.setOnClickListener(null)
        else loginButton.setOnClickListener {
            lifecycleScope.launch {
                intents.send(
                    RequestLogin(
                        "${userNameEditText.text}",
                        "${passwordEditText.text}"
                    )
                )
            }
        }
    }


    /**
     * here we need the Intents Channel to be able to send user events to it,
     * which will cause the UI Models to be updated later
     */
    private fun updateRegisterButton(model: Model, intents: Channel<Intents>) {
        if (model.progress) registerButton.setOnClickListener(null)
        else registerButton.setOnClickListener {
            lifecycleScope.launch {
                intents.send(
                    RequestRegister(
                        "${userNameEditText.text}",
                        "${passwordEditText.text}"
                    )
                )
            }
        }
    }


}


In onCreateView(), we first got the ViewModel class that holds the Channels, and then we launched a coroutine on the Main thread to start listening to the UI Models Channel updates, and also the Intents Channel are used to send the new Actions that happens

so MVI is like a cycle of events, we have a View function that is listening to the UI Models updates, and if any action happens it updates the Intents Channel which as a result, will cause the UI Models to be updated (and update the Views) again

and the Cycle goes on

We could have made the code in our LoginViewModel init() as a function, that takes an intents Channel as a parameter, and return the UI Models Channel as a result, and call this function "model", and in this case you can see the pattern like implemented like this in our Fragment :

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

which is the original syntax displayed by Andre Staltz in his presentation for the pattern in Cycle-JS framework

Another example for the same screen that is more of a functional approach and also more Kotlin-ish can be found here

Waqar Yasin

App | Android | Java | Kotlin l MVVM l Room l Hilt l VOIP

3 年

sorry brother i did't understand can i have i have your time .i need to ask few question related to mvi?

回复
Mahmoud Elrasool

Quality manager at Majid Al-Futaim, Data analyst by nature

5 年

Good evening, Mr. Ahmed, I hope you are doing well. Actually, I didn't make any final android app to the end, and as far, I can't imagine how can I use such concepts, on top of that, I've read a lot of books about those concepts but, unfortunately, didn't understand any of them, I even started reading Head first design patterns which recommended by lots. So, how can I conquer them and use them effectively? Thanks in advance.

回复
Mostafa Yehya

Senior Software Engineer I @ Careem | Java, Clean Coding

5 年

Great explanation, but how this is any better than MVVM? or to put it in other words? when to user MVI vs MVVM ??

要查看或添加评论,请登录

Ahmed Adel Ismail的更多文章

  • Sharing data across multiple Mobile Squads - with examples

    Sharing data across multiple Mobile Squads - with examples

    Earlier I shared an article suggesting a solution to a common problem with teams following the "Spotify Model", which…

  • SDD - Squad Driven Design

    SDD - Squad Driven Design

    Working in multiple big teams I've found that we are always trying to apply our known best practices in software, but…

    4 条评论
  • Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Before we start, this article requires basic knowledge about the following topics : Clean Architecture Unit Testing…

    10 条评论
  • Android - A Cleaner Clean Architecture

    Android - A Cleaner Clean Architecture

    It has been a while now since Clean Architecture was out, and even many of us started embracing hexagonal (ports and…

    10 条评论
  • Beyond Functional Programming

    Beyond Functional Programming

    In the Android industry, lately functional programming was the all new stuff to learn, RxJava, Kotlin, and the whole…

    7 条评论
  • Dependency Injection in Clean Architecture

    Dependency Injection in Clean Architecture

    After Google's Opinionated Guide to Dependency Injection Video, Google made a clear statement that they want developers…

    18 条评论
  • Meta Programming in Android

    Meta Programming in Android

    Year after year we are getting rid of the boilerplate code that we need to write for small and simple tasks in Android,…

    2 条评论
  • Agile - Moving Fast

    Agile - Moving Fast

    We always here about Agile, and think about which methodology do we use, what practices do we have, team velocity…

    1 条评论
  • Kotlin Unit Testing with Mockito

    Kotlin Unit Testing with Mockito

    I've always believed that, if the code is designed to be tested, we wont need any testing framework or library ..

    17 条评论
  • MVI - Model View Intent simplified

    MVI - Model View Intent simplified

    I have been searching for a proper resource to explain the MVI pattern, but every time I get hit with Hannes Dorfmann…

    4 条评论

社区洞察

其他会员也浏览了