Chat with us, powered by LiveChat
App'n'roll blog Menu

Build Your own MVI Framework

This article was first published by Mariusz Dąbrowski in the App’n’roll Publication on Medium.

Model View Intent (MVI) architecture is getting more and more attention in the Android developer community. In this article I will demonstrate how to create a simple framework using this pattern.

I will not be describing the idea behind the MVI pattern in detail. There are some great articles that can be found herehere and here. I will focus strictly on the source code of a framework.

The sample application with source code used in this article can be found here.
Code snippets are written in Kotlin.


Do I need a “custom” framework?

There are some libraries which can be used to build Android applications with Model View Intent architecture, for example:

I would strongly encourage that you check out the source code of those libraries. Although, remember that using them in a production project will create a strong dependency between a crucial part of an application (an architecture) and a 3rd party library, which can not be controlled by the app developer.

It is quite safe to use libraries like RetrofitMPAndroidChartDagger or Koin — as it is very hard to write a custom version with similar functionalities. A custom architecture framework is relatively easy to write, it gives you more flexibility and can be adjust by the developer, depending on the needs of their application. Writing one will also give you a better understanding of how the pattern works from the inside. So, let’s start to code.


Action, Result, ViewState

MviActionMviResult and MviViewState are the core interfaces of the framework. Each screen in the app should contain its own ViewState and its own sets of Actions and Results.

MviAction 
Action represents all interactions with the app, for example if you click on a button or swipe to refresh data. It is called Action, not Intent, in order not to confuse it with Android Intent which is a completely different component.

MviResult
Result contains the output of a performed action, for example a collection of items fetched from backend, in response to refresh data action. It can also contain an error message if the data can not be fetched. One Action can produce zero or more Result objects.

MviViewState 
ViewState represents the whole UI state of a screen. In MVI, the visual part of an application can only be changed in response to changes in ViewState. This class should contain all information needed to properly render the whole screen.

MviViewState interface has two metods:

    • reduce(result) — in MVI architecture, new ViewState can only be created by merging the previous one with new Result object. This functionality is called reducing and it can be implemented by a separate class called Reducer. But in order to minimize the number of classes, it can also be done by a ViewState itself. Reducing is nothing more than just creating new ViewState object with the data from Result object. No business logic should be put in this process — it should be a simple, pure function.
    • isSavable() — this additional method can tell if the current ViewState can be saved when ViewModel is recreated. This should not be confused with Activity or Fragment recreation, because in those cases ViewModel is still alive and the last ViewState is kept there. However, in a case when the Android System runs out of resources it can destroy ViewModel in order to release them. This scenario can be mimicked by turning on “Do not keep activities” in the Developers Menu. Then, if the application is closed using the Home Button and opened again, the new instance of ViewModel will be created. In order to preserve the application state even through ViewModelrecreation, the last rendered ViewState can be saved in onSaveInstanceState(…) method and than provided as a initial state to the newly created ViewModel (that is why MviViewState interface implements Parcelable). The distinction between which ViewState should be savable and which should not, is simple. If the ViewState represents an asynchronouse operation (like loading data) than it should NOT be savable. If you do so, then after ViewModel recreation the loading state will be rendered but there will be no pending operation, which will produce a new Result with the data.

ActionProcessor

MviActionProcessor is responsible for producing Results in response to user Actions. The framework is based on RxJava, so this component is a subclass of ObservableTransformer which is taking the stream of Actions and produces the stream of Results.

Function apply() is overridden and marked as final in order not to break the processor flow in its subclasses. The idea is that each Action should have it’s own ActionProcessor — which makes a lot of sense as each Action should be different. Operator publish is able to send an Action to an appropriate processor based on its type. A list of processors for all Actions should be returned by getActionProcessors(…) function and each element of this list is created created by executing a connect(actionProcessor) extension function on a shared stream of Actions.

It may be hard to undarstand based on the description so here is an example:

Each processor responds with a proper Result for a particular Action. Operator switchMap(…) is used to cancel processing and start a new one, if the new Action will be emitted during processing the previous one. A full example can be found here.

ActionProcessor is the place where business logic should be implemented and it is the place where interactions with the data layer should take place (local Database or remote API). If an application is using UseCases as a business logic implementation then those UseCases should be executed here. ActionProcessor should also be aware of threads — the data layer should be accessed on a Worker thread but Results should be emitted on a UI thread.

ActionProcessor is emitting a stream of Results in response to one Action. This makes asynchronous operations very easy to implement and maintain. If the application needs to fetch data from a remote API, then a proper ActionProcessor can emit (at the very beginning) InProgressResult — in order to tell the UI that it needs to render a progress bar. Then after the data is loaded, the processor can emit another object — DataResult with proper data, which will tell the UI to hide the progress bar and display a list of items.

ViewModel

MviViewModel is a class which binds all of the MVI components together. It is surviving Activity and Fragment recreation, so the current ViewState is preserved by default. MviViewModel constructor is taking two parameters:

    • actionProcessor — which is described in the previouse section
    • defaultState — reducing is the process which creates a new ViewStatebased on a previous one and an emited Result object— so there always needs to be some default ViewState from which the process can start.

MviViewModel class parameters description:

    • disposables — ViewModel is subscribing to Actions Observable provided by the UI and using disposables we can unsubscribe, when we no longer want to proceed any further with Actions.
    • actionSource —An object used as both a consumer for Actions provided by the UI and a source for the Actions processed by the ActionProcessor.
    • viewStateObservable — Observable with ViewStates to which the UI can subscribe in order to receive updates.

MviViewModel methods description:

    • initViewStatesObservable(savedViewState) —is initializing the ViewStateObservable. Initialy the ViewState can be provided as a parameter — saving the ViewState process was described by the MviViewState::isSavable()method description at the beginning of this article.
    • getViewStatesObservable() —is returning to the UI of the ViewStatesObservable — or throwing an error if it was not properly initialized.
    • processActions(actionsObservable) —using this method the UI component can provide an Actions Observable to the MviViewModel.
    • clear() — is disposing the subscription to Actions Observable.

Initiating ViewState Observable process step by step:

  1. if (viewStatesObservable == null) — initiate only if it was not initiated before. On the one hand, ViewState Observable should be initiated in Activity::onCreate() or Fragment::onViewCreated() method, however if Activity or Fragment are recreated, the ViewModel is still alive and ViewState Observable is already initiated (when Activity::onCreate() or Fragment::onViewCreated() are executed). On the other hand, it can be easly changed, if required, and new ViewState Observable can be created on each Activity or Fragment creation.
  2. viewStateObservable = actionSource — assign the final Observable to ViewState Observable variable.
  3. .compose(actionProcessor) — transform an Action from ActionsObservable using provided ActionProcessor. This operator will create a Results Observable.
  4. .scan<VS>(...) { ... } — use savedViewState (or defaultViewState if saved one is null) as an initial ViewState, and on each emition from ResultsObservable (created in the previouse line), it will execute reduce function from the current ViewState and with the emited Result as an argument. At the end it will emit newly created ViewState. At this point ViewStateObservable is created.
  5. .distinctUntillChanged() — block emition of ViewState if it is exacly the same as the previously emitted one. It will prevent from rerendering the same ViewState in UI.
  6. .replay(1) — transform Observable into ConnectableObservable with buffer size 1. Now when a UI component unsubscribes from the ViewStateObservable and subscribes again, the last emited ViewState will be reemitted.
  7. .autoConnect(0) — because the replay() operator transforms Observable into ConnectableObservables, the connect() operator needs to be invoked in order to start emission after the Consumer subscribes to it. This operator tells the ObservableSource that the emission should starts right after the Consumer subscription and the operator argument determinates how many Consumers need to be subscribed in order to start the emition (in this case the allowed values are 0 or 1).

MviController

All components described above (MviActionMviResultMviViewStateMviActionProcessor and MviViewModel) are enough to implement MVI pattern inside an Android application. Activity or Fragment can now:

    1. Instantiate proper MviViewModel and MviActionProcessor
    2. Subscribe to its ViewStates Observable
    3. Provide own Observable with Actions 
    4. Handle disposing of ViewStates and Actions Observables subscriptions
    5. Handle saving/restoring ViewStates on recreation

It is only a few steps to implement but adding this routine to every Activity and Fragment will produce a lot of boilerplate code. That is why in order to reduce it, the whole flow can be done by a new component — MviController.

The controller takes four parameters:

    • viewModelProvider — provider for ViewModel(s).
    • viewStateParcelKey — name of the key under which the ViewState object will be written in the Bundle during saving state.
    • lifecycle — MviViewController implements LifecycleObserver interface so it needs a Lifecacle object in order to automatically register itself.
    • callback — instance of MviControllerCallback object, which is presented below.

MviControllerCallback interface needs to be implemented by the Activity or Fragment which wanted to use MviController. It has two methods:

    • initialAction(lastViewState) — it is invoked in onStart() method of a Activity or Fragment lifecycle and tt returns an Action which should be emitted at that moment. For example: an Action that will fetch the data from the datasource. If null is returned than no Action will be emitted.
    • render (viewState)—it is called each time new ViewState is emitted. It should be used to change the UI of a Fragment or Activity.

MviController public methods description:

    • initViewModel(…) — instantiate internal ViewModel object based on class provided as an argument. ViewModel class must be a subclass of MviViewModel. This method also registers the controller in the provided Lifecycle and it should be invoked in onCreate() method of Activity or Fragment.
    • initViewStateObservable(…) — initiate ViewStates Observable in the ViewModel object. It will also read the saved ViewState object from the Bundle and will use it as an initial one. If there is no saved ViewState, it will use the last rendered ViewState which is kept localy (or null if nothing was yet rendered). This method should be invoked after initViewModel(…)and it should be done in Activity::onCreate() and Fragment::onViewCreated().
    • accept(…) — it can be used by an Activity or Fragment in order to pass Actions that should be emitted, for example button clicks.
    • saveLastViewState(…) — it will save thelast rendered ViewState in the Bundle. This method should be invoked in onSaveInstanceState(…)method of Activity or Fragment.

MviController lifecycle aware methods:

    • onStart() — is invoked on the ON_START event of the lifecycle. It is subscribing with the render(…) function to the ViewStates Observable and it passes an Actions Observable to the ViewModel. Next, it invokes initial Action method from the callback.
    • onStop() — is invoked on the ON_STOP event of the lifecycle It disposes the subscriptions in MviController and in ViewModel

MviController other methods:

    • render(viewState) — this method is used when subscribing to the ViewStates Observable. It delegatetes the rendering tasks to the MviControllerCallback object, and it also saves the rendered ViewStateinternally in order to use it later when saving ViewState to bundle is executed.

MviActivity, MviFragment

In order to reduce boilerplate code even more, the base classes for Activity and Fragment can be created:

The code is rather self explanatory. Both MviActivity and MviFragment are implementing MviControllerCallback interface, then they lazily created the MviController instance to execute its functions, in the proper lifecycle callbacks. The controller object has avisibility modifier: protected, so it can be accessed in subclasses for example in order to pass an Action.

An example of usage of this framework can be found here.

Conclusion

Wirting a custom framework requires some effort, however it can drasticly reduce the boilerplate code inside the project and speed up the development. It will also give you a chance to design the API which will fulfill the needs of the application. After writing it once it can also be used in other projects with or without modifications. I encourage you to try it in your own projects.

If you’ve found this article useful please don’t hesitate to share it and if you have any further questions, feel free to comment below.


Bonus!

Two great talks about Model View Intent pattern and managing state:



Close