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> { } fun view ( actions: BehaviorSubject<Action>, model: Model ) { }

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