How to prepare MVI in 2024 part 1

Android Insights

Introduction

It so happens that I am a big fan of unidirectional architectures, namely MVI, and here's why:

  • Predictability and condition control

    MVI makes application state completely manageable. Each state change occurs through certain actions (Intent), and the state is updated through the reducer. This makes monitoring and debugging easier because at any given time the state of the application is known and can be reconstructed.

  • One entry point for state management

    In MVI, state is controlled by only one data flow (Unidirectional Data Flow). All logic for working with application state is concentrated in one place, making the code easier to understand and maintain.

  • Simplified debugging and testing

    With a single source of truth and strict state change rules, MVI makes it easy to write unit tests. It's easier to reproduce scripts for testing because each action produces known changes.

But like everything in this world, MVI is not without its drawbacks

  • Increased structural complexity

    MVI is a less common architectural pattern compared to MVP or MVVM and may require more time for developers to master. It can be especially difficult for those who are not familiar with functional programming or reactive streams.

  • Redundancy for simple applications

    For small applications with limited logic, state may be easier to manage using lighter architectures such as MVVM, where the complex data flow design and strict declarative model are not required.

For me personally, the existing disadvantages are offset by the advantages and capabilities of the MVI approach. I discovered this architecture for myself quite a long time ago, and since then I have been actively practicing and applying it.

There are already quite a few ready-made solutions on the library market that could potentially meet your exact needs, but the more experienced I became, the more shortcomings and inconveniences I noticed for myself. I have no desire or purpose to disparage the merits of other solutions, so I will not dwell on this.

One way or another, I came to the point that I decided to create my own solution.

So, I present to your attention SimpleMVI

But before we start looking at the library, let's remember (or study) what is this MVI of yours in general?

What is MVI?

MVI (Model-View-Intent) is an architectural pattern used in mobile application development that focuses on managing application state through data streams. In MVI, all interactions can be broken down into three main components:

  • Model – this is the part that is responsible for storing the application state and the logic for changing it. State in MVI is considered the single source of truth, making the application easier to control and predictable.

  • View is the user interface. It displays the current state (Model) and reacts to changes. The View does not contain logic, but only shows the data received from the Model.

  • Intent are user actions or events that generate new changes in state. Intents are passed to the Model for processing and state changes.

MVI emerged as an evolution of architectural patterns for managing state in mobile and web applications. The roots of this approach can be traced back to architecture Cycle.js and the principles that were used in the pattern Redux for JavaScript.

The basic idea of ​​MVI is unidirectional data flow and state management through continuous streams. The pattern came to mobile development as a more modern and cleaner alternative to traditional patterns like MVC and MVP, which often led to difficulties in scaling and testing. MVI was inspired by the success of the Redux library, which demonstrated the effectiveness of unidirectional data flow for managing the state of interfaces.

Thus, MVI is the result of the search for a more predictable and manageable approach to application architecture, especially in the face of complex user interfaces with a large number of states and interactive elements.

Now that we understand what MVI is and where it came from, it's time to get back to its implementation, namely SimpleMVI!

What is SimpleMVI?

SimpleMVI is a modern, flexible and powerful library that helps implement MVI architecture in your projects. When designing the library, I tried to make it as easy to use as possible, which is reflected in the name. SimpleMVI has only two main interfaces, the first of which is Store

Store interface

First, let's show you what this interface looks like.

Hidden text
public interface Store<in Intent : Any, out State : Any, out SideEffect : Any> {

    public val state: State

    public val states: StateFlow<State>
  
    public val sideEffects: Flow<SideEffect>

    @MainThread
    public fun init()

    @MainThread
    public fun accept(intent: Intent)

    @MainThread
    public fun destroy()
}

Each Store must be parameterized with three parameters:

  • Intent – defines the actions that the Store can process

  • State – describes the state of the Store

  • SideEffect – events that can occur inside the Store during Intent processing, but do not lead to a change in State

The current state of the Store can be obtained by calling the property stateyou can also subscribe to the Store state stream using the property states.

Store can also produce SideEffect. A SideEffect is an event that can happen and you want to communicate it to the outside world, but at the same time this event does not cause the state of the Store to change.

Each Store must be initialized via a function call init(), and destroyed by calling the function destroy()

You can create a Store by calling the function createStore

Hidden text
public fun <Intent : Any, State : Any, SideEffect : Any> createStore(
    initialize: Boolean = true,
    coroutineContext: CoroutineContext = Dispatchers.Main.immediate,
    initialState: State,
    initialIntents: List<Intent> = emptyList(),
    middlewares: List<Middleware<Intent, State, SideEffect>> = emptyList(),
    actor: Actor<Intent, State, SideEffect>,
): Store<Intent, State, SideEffect> {
    return DefaultStore(
        coroutineContext = coroutineContext,
        initialState = initialState,
        initialIntents = initialIntents,
        middlewares = middlewares,
        actor = actor,
    ).apply {
        if (initialize) {
            init()
        }
    }
}

Let's look at what each parameter of this function is responsible for.

  • initialize – determines whether the Store will be initialized immediately upon creation

  • coroutineContext – coroutine context in which the Store will work

  • initialState – initial state of the Store

  • initialIntents – list of Intents that will be processed after initialization

  • middlewares – list of Middleware for this Store, will be discussed in further articles

  • actor is the second basic interface in SimpleMVI. This object implements logic, let’s look at it in more detail

Actor interface

Hidden text
public interface Actor<Intent : Any, State : Any, out SideEffect : Any> {

    @MainThread
    public fun init(
        scope: CoroutineScope,
        getState: () -> State,
        reduce: (State.() -> State) -> Unit,
        onNewIntent: (Intent) -> Unit,
        postSideEffect: (sideEffect: SideEffect) -> Unit,
    )

    @MainThread
    public fun onIntent(intent: Intent)

    @MainThread
    public fun destroy()
}

The Actor is responsible for implementing the logic of a specific Store. It handles Intent processing, can create a new State and send a SideEffect.

The interface itself contains only three functions:

  • init – binds the Actor to the Store and initializes it

  • onIntent – this function is called when the Store receives a new Intent

  • destroy – called when the Store is destroyed, resources can be cleared in it

Example of creating a Store

Creating a Store is quite simple, the recommended way is to Declare a class, e.g. MyCoolStorecreate classes that describe Intent, State And SideEffect For MyCoolStore.

Also MyCoolStore must implement the interface Store.

To implement the Store interface, you can use delegation, which I talked about in my last article.

This is what the final result will look like:

Hidden text
class MyCoolStore : Store<MyCoolStore.Intent, MyCoolStore.State, MyCoolStore.SideEffect> by createStore(
    initialState = State(),
    actor = actorDsl {
        onInit { /* code */ }

        onIntent<Intent.DoCoolStuff> { intent -> /* code */ }

        onDestroy { /* code */ }
    },
) {
    sealed interface Intent {
        data object DoCoolStuff : Intent
    }

    data class State(
        val value: Int = 0,
    )

    sealed interface SideEffect {
        data object SomethingHappened : SideEffect
    }
}

This example uses the dsl function to create an Actor actorDsl. This dsl allows you to:

  • execute the lambda at the time of Store initialization, passed to the function onInit

  • execute the lambda when the Store is destroyed, passed to the function onDestroy

  • process each received Intent by declaring a handler by calling the onIntent function

It is important to remember that you can register handlers only once, otherwise the library will throw an exception.

Conclusion

This information is enough for your first acquaintance with the library. SimpleMVI is available on maven central so you can easily connect it to your projects

In the next part of the article I will dive into the implementation details and features of the library

Source code and examples are available in the repository: SimpleMVI

PS

I will be glad to see everyone on my Telegram channel

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *