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 Mosby examples, which is some how complex, and for some reason He is trying to re-use the already existing Mosby library, which is increasing the complexity of the pattern (from my point of View)

Why Model-View-Intent

Based on André Staltz, the man behind this pattern, the objective of the pattern is to do what MVC does, but in a reactive functional way, as elaborated in this link

And he describes the pattern to be as follows :

Instead of dividing our code into a View, Model, and Controller classes, we divide them into 3 functions, and using Reactive Extensions (Rx), we can make those three functions observe on each other and react to each other

For us (Android Developers), the main() function in the above image is the onCreate() of our Activity/Fragment, so we can translate the above image in Android as follows :

override fun onActivityCreated(savedInstanceState: Bundle?) {
    ...
    val actions = BehaviorSubject.createDefault(Action())
    intent(actions).subscribe { model -> view(actions, model) }
}

fun intent(actions: BehaviorSubject<Action>) : Observable<Model>{
    // subscribe on the actions emitted,// and emits new Model objects based on every action
}

fun view(actions: BehaviorSubject<Action>, model: Model){
    // update the views from the new Model received, // and emits user actions through actions.onNext(Action) when // an action occurres, like onClick for example
}
 

So The cycle goes like this :

The view() function does 2 things :

  • Draw the Views based on the Model object received in it's parameter
  • Set the click listeners, touch listeners, etc.. to call actions.onNext() with there related Actions

The intent() function acts as the Controller in the MVC but with slight difference:

  • Subscribe on the Actions emitted by the BehaviorSubject, and based on the emitted action, it creates a new Model Object with the new UI expected state ... it may need to make a network call or a database call, or some calculations, and based on this operation, it creates a new Model instance to be drawn on the UI

And the cycle keeps going, but there are some limitations :

1- We have life-cycle events in Android, and this will require declaring variables in the Activity/Fragment class, which is not possible in such pattern, there is a library that helps handling Lifecycle events in a functional way, called Litecycle

2- This pattern does not support rotation, so it should be done in a Fragment that calls it's setRetainInstance(true) in it's creation ... in other words, it is not possible to use it in Activities for Android ... luckily enough we have the new Navigation in Jetpack which makes use of single Activity Architecture, where all the screens are Fragments

Sample Repository for this pattern can be found here, and sample code will be as follows :

class SplashFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_splash, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        LiteCycle.with(integration())
            .forLifeCycle(this)
            .onDestroyInvoke(Disposable::dispose)
            .observe()
    }

    private fun integration() = intent(fragmentStartedObservable()).subscribe { view(it) }

    private fun fragmentStartedObservable() = LiteCycle.with(false)
        .forLifeCycle(this)
        .onStartUpdate { true }
        .onStopUpdate { false }
        .observe(BehaviorSubject.create())

}


fun intent(fragmentStartedObservable: Observable<Boolean>) = fragmentStartedObservable
    .switchMap { Observable.just(it).delay(2, TimeUnit.SECONDS) }!!


fun SplashFragment.view(finished: Boolean) = findNavController()
    .takeIf { finished }
    ?.apply { navigate(R.id.action_splashFragment_to_loginFragment) }

In the above example, The Action is a boolean type, of weather the Fragment has started or not. and also the Model is a boolean type, of weather the splash screen shall navigate to the next screen or not

The pattern is applied in the integrate() function, which invokes :

intent(fragmentStartedObservable()).subscribe { view(it) }

A more advanced sample will be the Login screen :

class LoginFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_login, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        LiteCycle.with(integration())
            .forLifeCycle(this)
            .onDestroyInvoke(Disposable::dispose)
            .observe()
    }

    private fun integration() = LoginViewState()
        .let(::Initialize)
        .let { BehaviorSubject.createDefault<LoginAction>(it) }
        .let { IntentData(it) }
        .let { intent(it).subscribe { state -> view(it.actions, state) } }

}

fun intent(data: IntentData): Observable<LoginViewState> = data.actions
    .subscribeOn(data.backgroundScheduler)
    .observeOn(data.backgroundScheduler)
    .switchMap { handleIntent(it, data.loginRequest) }
    .observeOn(data.mainScheduler)!!


private fun handleIntent(action: LoginAction, loginRequest: (String?, String?) -> Observable<User>) =
    when (action) {
        is Initialize -> Observable.just(LoginViewState(initialize = true))
        is LoginRequest -> processLoginRequest(loginRequest, action)

    }

private fun processLoginRequest(loginRequest: (String?, String?) -> Observable<User>, action: LoginRequest) =
    loginRequest(action.state.userName, action.state.password)
        .map { user -> LoginViewState(loginResponse = user) }
        .onErrorReturn { throwable -> LoginViewState(errorMessage = throwable.message) }


fun LoginFragment.view(actions: BehaviorSubject<LoginAction>, viewState: LoginViewState) = with(viewState) {
    login_progress.visibility = if (progressing) View.VISIBLE else View.INVISIBLE
    when {
        initialize -> login_button.setOnClickListener { handleOnClick(this, actions) }
        errorMessage != null -> Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show()
        loginResponse != null -> findNavController().navigate(R.id.action_loginFragment_to_homeFragment)
    }
}

private fun LoginFragment.handleOnClick(state: LoginViewState, actions: BehaviorSubject<LoginAction>) = with(state) {
    when {
        progressing -> Toast.makeText(context, "please wait", Toast.LENGTH_SHORT).show()
        loginResponse == null -> startLoginRequest(actions)
        else -> Toast.makeText(context, "login success", Toast.LENGTH_SHORT).show()
    }
}

private fun LoginFragment.startLoginRequest(actions: BehaviorSubject<LoginAction>) {
    login_progress.visibility = View.VISIBLE
    LoginViewState(userName = user_name_edit_text.asString(), password = password_edit_text.asString())
        .let(::LoginRequest)
        .also(actions::onNext)
}


data class IntentData(
    val actions: BehaviorSubject<LoginAction>,
    val loginRequest: ((String?, String?) -> Observable<User>) = ::login,
    val backgroundScheduler: Scheduler = Schedulers.io(),
    val mainScheduler: Scheduler = AndroidSchedulers.mainThread()
)

sealed class LoginAction
data class Initialize(val state: LoginViewState) : LoginAction()
data class LoginRequest(val state: LoginViewState) : LoginAction()

data class LoginViewState(
    val initialize: Boolean = false,
    val progressing: Boolean = false,
    val userName: String? = null,
    val password: String? = null,
    val errorMessage: String? = null,
    val loginResponse: User? = null
)


 
  

It may look strange at the beginning, but the concept is the same, to make all the events go through one direction, with the help of RxJava and LiteCycle, we can achieve this pattern in a purely functional way

When to use this pattern ?

When you want to make a UI pattern that is applying to Functional Programming concepts, it guarantees that events are executed in a loop - similar fashion, one after another, and in a very planned and expected way

Dipendra Sharma

Mobile Application Architect | Android, iOS, Flutter Expert | Agile & Open Source Advocate

6 年

Superb Article. Thanks for writing.

Vikram Singh

11+ YoE || Fintech || Mobile Architect { Android, Multiplatform, KMP, React Native, Flutter } || Security

6 年

Lovely article, must read

Ahmed Ashraf

Software Engineer | Android

6 年

This pattern is for sure a competitor to the other MV* patterns for the F/R programming fans. However, I have a concern about using "setRetainInstance(true)" to a fragment that will be added to the backstack, and with the single-activity approach in particular. https://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean Also, the pattern seems to use the all-in-one-place hell, all the binding+presentation logic is in the view, and indeed very similar to MVC at this specific point, and of course providing extra extensions to the View will not solve the issue, I've seen this done by iOS devs quite often when things start to get out of hand in their UIViewControllers too.

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

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 条评论
  • MVI Pattern For Android In 4 Steps

    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…

    7 条评论
  • 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 条评论

社区洞察

其他会员也浏览了