📜 ⬆️ ⬇️

MVIDroid: review of the new MVI library (Model-View-Intent)

Hello! In this article I want to talk about the new library, which introduces the MVI design pattern in Android. This library is called MVIDroid, written 100% in the Kotlin language, lightweight and uses RxJava 2.x. I am the author of the library personally, its source code is available on GitHub, and you can connect it via JitPack (link to the repository at the end of the article). This article consists of two parts: a general description of the library and an example of its use.


MVI


And so, as a preface, let me remind you what MVI is. Model - View - Intent or, if in Russian, Model - View - Intention. This is a design pattern in which the Model is the active component, which takes Inputs and Inputs the State. View (View), in turn, takes View Models (View Model) and produces those very Intentions. The state is transformed into a View Model using a transformer function (View Model Mapper). Schematically, the MVI pattern can be represented as follows:


MVI


The MVIDroid View does not produce Intentions directly. Instead, it produces View Events (UI Events), which are then converted into Intentions using a transformer function.


View


Main components of MVIDroid


Model


Let's start with the model. In the library, the concept of Model is slightly expanded; here it produces not only States but also Labels. Tags are used to communicate the models between themselves. The labels of some Models can be transformed into the Intentions of other Models with the help of transforming functions. Schematically, the Model can be represented as follows:


Model


In MVIDroid, the Model is represented by the MviStore interface (the name Store is borrowed from Redux):


interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean } 

And so what we have:



Note that all Model methods must be run on the main thread. The same is true for any other component. You can perform background tasks, of course, using standard RxJava tools.


Component


A component in MVIDroid is a group of Models united by a common goal. For example, you can select in the Component all Models for any screen. In other words, the Component is a facade for the Models enclosed in it and allows to hide implementation details (Models, transforming functions and their links). Let's look at the Component scheme:


Component


As can be seen from the diagram, the component performs the important function of transforming and forwarding events.


The full list of Component functions is as follows:



The component also has its own interface:


 interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean } 

Consider the component interface in more detail:



View


As you can guess, the view is needed to display data. The data for each View is grouped into a View Model and is usually represented as a data class (Kotlin). Consider the Presentation interface:


 interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable } 

Here everything is somewhat simpler. Two Generic parameters: ViewModel is the Type of the View Model and UiEvent is the Type of the View Events. One uiEvents field is Observable Event View, enabling clients to subscribe to these same events. And one method of subscribe (), giving the opportunity to subscribe to the View Model.


Usage example


Now is the time to try something in practice. I propose to do something very simple. Something that does not require much effort to understand, and at the same time will give an idea of ​​how to use all this and in which direction to move on. Let it be a UUID generator: by pressing a button we will generate a UUID and display it on the screen.


Representation


To begin with we will describe Model of Representation


 data class ViewModel(val text: String) 

And Presentation Events:


 sealed class UiEvent { object OnGenerateClick: UiEvent() } 

Now we are implementing the View itself; for this we need inheritance from the abstract class MviAbstractView:


 class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).distinctUntilChanged().subscribe { textView.text = it } } 

Everything is very simple: we subscribe to changes in UUID and update TextView when we receive a new UUID, and on pressing the button we send the OnGenerateClick event.


Model


The model will consist of two parts: the interface and the implementation.


Interface:


 interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } } 

Everything is simple here: our interface extends the MviStore interface, indicating the types of State (State) and Intentions (Intent). Type of Labels - Nothing, since our Model does not produce them. The interface also contains classes of states and intentions.


In order to implement the Model, you need to understand how it works. Inputs to the Model are Intentions (Intent), which are converted into Actions (Action) using the special function IntentToAction. Actions come to the input to the Executor, which executes them and produces Results (Result) and Labels. The results are then transferred to a Reducer, which converts the current State to a new one.


All four make models:



To create the Model itself, it is necessary to use a special Factory of Models. It is represented by the MviStoreFactory interface and its implementation of MviDefaultStoreFactory. The factory takes the constituent Models and delivers a ready-to-use Model.


The factory of our Model will look as follows:


 class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } } 

This example shows all four components of the Model. First, create a factory method, then Actions and Results, followed by the Artist and at the very end of the Reducer.


Component


The states of the Component (the States group) are described by the data class:


 data class States(val uuidStates: Observable<UuidStore.State>) 

When adding new Models to the Component, their States should also be added to the group.


And, actually, the implementation itself:


 class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } } 

We inherited the abstract class MviAbstractComponent, specified the States and Event types of the View, passed our Model to the super class, and implemented the states field. In addition, we have created a transforming function that will convert the Events of the View into the Intentions of our Model.


Mapping Models Representations


We have a State and Model of Presentation, it is time to convert one into the other. To do this, we implement the MviViewModelMapper interface:


 object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } } 

Communication (Binding)


The availability of the Component and View alone is not enough. For everything to start working, they need to be connected. It's time to create an Activity:


 class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).create()), View(this) using ViewModelMapper ) } } 

We used the bind () method, which accepts a Component and an array of Views with mappers of their Models. This method is an extension method over LifecycleOwner (which is Activity and Fragment) and uses DefaultLifecycleObserver from the Arch package, which requires Java 8 source compatibility. If for any reason you cannot use Java 8, then the second bind () method is suitable for you, which is not an extension method and returns MviLifecyleObserver. In this case, you have to call the life cycle methods yourself.


Links


The source code of the library, as well as detailed instructions for connecting and using it, can be found on GitHub .


')

Source: https://habr.com/ru/post/417763/


All Articles