Skip to the content.
Overview Store View Binding and Lifecycle State preservation Logging Time travel

Store

Store is the place for business logic. In MVIKotlin it is represented by the Store interface which is located in the mvikotlin module. You can check its definition here.

It has the following features:

Observing states and labels

Usually you don’t need to use states(Observer) or labels(Observer) methods directly. There are extensions available for Reaktive and kotlinx.coroutines libraries. However, you will need those methods if you implement custom extensions. See also - Binding and Lifecycle.

Observing with Reaktive

Add the following dependency to your build.gradle file:

implementation("com.arkivanov.mvikotlin:mvikotlin-extensions-reaktive:<version>")

Now you can observe states and labels using the following extensions:

Observing with kotlinx.coroutines

Add the following dependency to your build.gradle file:

implementation("com.arkivanov.mvikotlin:mvikotlin-extensions-coroutines:<version>")

Now you can observe states and labels using the following extensions:

Store structure

Every Store has up to three components: Bootstrapper, Executor and Reducer. Here is the diagram of how they are connected:

Store

Bootstrapper

This component bootstraps (kick-starts) the Store. If passed to the StoreFactory it will be called at some point during Store initialization. The Bootstrapper produces Actions that are processed by the Executor. The Bootstrapper is executed always on the main thread, Actions must be also dispatched only on the main thread. However you are free to switch threads while the Bootstrapper is being executed.

⚠️ Please note that Bootstrappers are stateful and so can not be objects (singletons).

Executor (version 4.x)

This is the place for business logic, all asynchronous operations also happen here. Executor accepts and processes Intents from the outside world and Actions from inside the Store. The Executor has three outputs: Messages, Action and Labels. Messages are passed to the Reducer, Actions are forwarded back to the Executor itself, Labels are emitted straight to the outside world. The Executor has constant access to the current State of the Store, a new State is visible for the Executor right after the Message is dispatched. The Executor is executed always on the main thread, Messages and Labels must be also dispatched only on the main thread. However, you are free to switch threads while processing Action or Intents.

⚠️ Please note that Executors are stateful and so can not be objects (singletons).

Executor (version 3.x)

This is the place for business logic, all asynchronous operations also happen here. Executor accepts and processes Intents from the outside world and Actions from the Bootstrapper. The Executor has two outputs: Messages and Labels. Messages are passed to the Reducer, Labels are emitted straight to the outside world. The Executor has constant access to the current State of the Store, a new State is visible for the Executor right after the Message is dispatched. The Executor is executed always on the main thread, Messages and Labels must be also dispatched only on the main thread. However, you are free to switch threads while processing Action or Intents.

⚠️ Please note that Executors are stateful and so can not be objects (singletons).

Reducer

This component is basically a function that accepts a Message from the Executor and the current State of the Store and returns a new State. The Reducer is called for every Message produced by the Executor and the new State is applied and emitted as soon as the Reducer call returns. The Reducer is always called on the main thread.

Creating a Store

Normally you don’t need to implement the Store interface directly. Instead you should use StoreFactory which will create a Store for you. All you need to do is to provide up to three components (Bootstrapper, Executor and Reducer) and an initial State. StoreFactory is used to abstract from a Store implementation. We can use different factories depending on circumstances and combine them as needed.

There are a number of factories provided by MVIKotlin:

Initializing a Store

During its initialization, the Store establishes internal connections and calls the Bootstrapper. By default Stores are initialized automatically by the StoreFactory. You can opt-out from the automatic initialization by passing autoInit=false argument to the StoreFactory.create(...) function.

⚠️ When automatic initialization is disabled, you should manually call the Store.init() method.

IDEA Live Templates

To speed up the creation of new Stores, you can use the following IDEA Live Templates. Download the archive and use the guide to import live templates into your IDE. You may need to restart the IDE after import.

⚠️ Safari browser may automatically unzip download archives, make sure that you import a zip file and not a folder.

Usage

Create a new Kotlin file and type one of the following abbreviations:

Simplest example

The following examples use the default full-featured API. For a simplified DSL, please refer to the corresponding section - Store DSL API.

Let’s start from a very basic example. We will create a simple counter Store that will increment and decrement its value.

The first thing we should do is to define an interface. This is how it will look:

internal interface CalculatorStore : Store<Intent, State, Nothing> {

    sealed interface Intent {
        object Increment : Intent
        object Decrement : Intent
    }

    data class State(
        val value: Long = 0L
    )
}

The CalculatorStore interface itself can be marked as internal, so it will be an implementation detail of a module. Also CalculatorStore has two Intents (Increment and Decrement) and the State with just a Long value. This is the public API of our Store.

Now it’s time for implementation:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    fun create(): CalculatorStore =
        object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create(
            name = "CounterStore",
            initialState = State(),
            reducer = ReducerImpl
        ) {
        }

    private object ReducerImpl : Reducer<State, Intent> {
        override fun State.reduce(msg: Intent): State =
            when (msg) {
                is Intent.Increment -> copy(value = value + 1L)
                is Intent.Decrement -> copy(value = value - 1L)
            }
    }
}

The only component we need is the Reducer. It accepts Intents and modifies the State by incrementing or decrementing its value. The factory function create() uses the StoreFactory which is passed as a dependency.

Adding Executor

Currently our CalculatorStore can only increment and decrement its value. But what if we need to calculate something? Let’s say we want to calculate a sum of numbers from 1 to N. We will need an additional Intent:

internal interface CalculatorStore : Store<Intent, State, Nothing> {

    sealed interface Intent {
        object Increment : Intent
        object Decrement : Intent
        data class Sum(val n: Int): Intent // <-- Add this line
    }

    data class State(
        val value: Long = 0L
    )
}

The idea is that CalculatorStore will accept Intent.Sum(N), calculate the sum of numbers from 1 to N and update the State with the result. But the calculation may take some time, so it should be performed on a background thread. For this we need the Executor.

So that our Executor could communicate with the Reducer we will need Messages:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    private sealed interface Msg {
        class Value(val value: Long) : Msg
    }
}

We will need a new Reducer because now it will accept Messages instead of Intents:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    private sealed interface Msg {
        class Value(val value: Long) : Msg
    }

    private object ReducerImpl : Reducer<State, Msg> {
        override fun State.reduce(msg: Msg): State =
            when (msg) {
                is Msg.Value -> copy(value = msg.value)
            }
    }
}

There is only one possible Msg.Value(Long) which just replaces whatever value in State.

Now it’s time for the Executor. If you are interested you can find the interface here. Luckily we don’t need to implement this entire interface. Instead we can extend a base implementation.

There are two base Executors provided by MVIKotlin:

Let’s try both.

ReaktiveExecutor

Version v4.0
internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    // ...

    private class ExecutorImpl : ReaktiveExecutor<Intent, Nothing, State, Msg, Nothing>() {
        override fun executeIntent(intent: Intent) =
            when (intent) {
                is Intent.Increment -> dispatch(Msg.Value(state().value + 1))
                is Intent.Decrement -> dispatch(Msg.Value(state().value - 1))
                is Intent.Sum -> sum(intent.n)
            }

        private fun sum(n: Int) {
            singleFromFunction { (1L..n.toLong()).sum() }
                .subscribeOn(computationScheduler)
                .observeOn(mainScheduler)
                .subscribeScoped { dispatch(Msg.Value(it)) }
        }
    }

    // ...
}
Version v3.0
internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    // ...

    private class ExecutorImpl : ReaktiveExecutor<Intent, Nothing, State, Msg, Nothing>() {
        override fun executeIntent(intent: Intent, getState: () -> State) =
            when (intent) {
                is Intent.Increment -> dispatch(Msg.Value(getState().value + 1))
                is Intent.Decrement -> dispatch(Msg.Value(getState().value - 1))
                is Intent.Sum -> sum(intent.n)
            }

        private fun sum(n: Int) {
            singleFromFunction { (1L..n.toLong()).sum() }
                .subscribeOn(computationScheduler)
                .observeOn(mainScheduler)
                .subscribeScoped { dispatch(Msg.Value(it)) }
        }
    }

    // ...
}

So we extended the ReaktiveExecutor class and implemented the executeIntent method. This method gives us an Intent and a supplier of the current State. For Intent.Increment and Intent.Decrement we simply send the Message with a new value using the dispatch method. But for Intent.Sum we use Reaktive for multithreading. We calculate the sum on the computationScheduler and then switch to the mainScheduler and dispatch the Message.

⚠️ ReaktiveExecutor implements Reaktive’s DisposableScope which provides a bunch of additional extension functions. We used one of those functions - subscribeScoped. This ensures that the subscription is disposed when the Store (and so the Executor) is disposed.

CoroutineExecutor

Version v4.0
internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    // ...

    private class ExecutorImpl : CoroutineExecutor<Intent, Nothing, State, Msg, Nothing>() {
        override fun executeIntent(intent: Intent) =
            when (intent) {
                is Intent.Increment -> dispatch(Msg.Value(state().value + 1))
                is Intent.Decrement -> dispatch(Msg.Value(state().value - 1))
                is Intent.Sum -> sum(intent.n)
            }

        private fun sum(n: Int) {
            scope.launch {
                val sum = withContext(Dispatchers.Default) { (1L..n.toLong()).sum() }
                dispatch(Msg.Value(sum))
            }
        }
    }

    // ...
}
Version v3.0
internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    // ...

    private class ExecutorImpl : CoroutineExecutor<Intent, Nothing, State, Msg, Nothing>() {
        override fun executeIntent(intent: Intent, getState: () -> State) =
            when (intent) {
                is Intent.Increment -> dispatch(Msg.Value(getState().value + 1))
                is Intent.Decrement -> dispatch(Msg.Value(getState().value - 1))
                is Intent.Sum -> sum(intent.n)
            }

        private fun sum(n: Int) {
            scope.launch {
                val sum = withContext(Dispatchers.Default) { (1L..n.toLong()).sum() }
                dispatch(Msg.Value(sum))
            }
        }
    }

    // ...
}

Here we extended the CoroutineExecutor class. The sum is calculated on the Default dispatcher and the Message is dispatched on the Main thread.

⚠️ CoroutineExecutor provides the CoroutineScope property named scope, which can be used to run asynchronous tasks. The scope uses Dispatchers.Main dispatcher by default, which can be overriden by passing different CoroutineContext to the CoroutineExecutor constructor. The scope is automatically cancelled when the Store is disposed.

Forwarding Actions

Starting with MVIKotlin version 4.0, it is also possible to send Actions from the Executor using forward(Action) method. The Action automatically redirected back to the Executor#executeAction method. This allows reusing Actions easier, and also proper processing by wrapping Stores (like logging or time-traveling).

Publishing Labels

Labels are one-time events produced by the Store, or more specifically by the Executor. Once published (emitted) they are delivered to all current subscribers and are not cached. The Executor has special method for it: publish(Label).

⚠️ If a Label is published straight from Executor.executeAction method, the Store may have no subscribers yet. This may happen if the Store is still being created, and the initialization is still in progress. Prefer manual Store initialization in this case.

Creating the Store

We also need to pass a factory of our Executor to the StoreFactory:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    fun create(): CalculatorStore =
        object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create(
            name = "CounterStore",
            initialState = State(),
            executorFactory = ::ExecutorImpl, // <-- Pass Executor factory
            reducer = ReducerImpl
        ) {
        }

    // ...
}

Why factory and not just an instance of the Executor? Because of the time travel feature. When debugging time travel events it creates separate instances of Executors when necessary and fakes their States.

⚠️ Please note that executorFactory should return a new instance of the Executor every time it is called.

Adding Bootstrapper

When we create a new instance of a Store it will stay in an initial State and do nothing until you supply an Intent. But sometimes we need to bootstrap (or kick start) a Store so it will start doing something once created. E.g. it can start listening for events from a server or load something from a database. This is why we need the Bootstrapper. As mentioned in the beginning the Bootstrapper produces Actions that are processed by the Executor the same way as Intents.

Our CalculatorStore is able to calculate sums of numbers from 1 to N. Currently it does this when Intent.Sum(N) is received. Let’s use the Bootstrapper to calculate sum(100) when the CalculatorStore is created. Our Executor already has everything to for sum calculation, so we can just send a triggering Action to the Executor, same as Intent.Sum(N).

Let’s first add an Action:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    // ...

    private sealed interface Action {
        class Sum(val n: Int): Action
    }

    // ...
}

Now it’s time to handle the Action in the ReaktiveExecutor:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    // ...

    private class ExecutorImpl : ReaktiveExecutor<Intent, Action, State, Msg, Nothing>() {
        override fun executeAction(action: Action, getState: () -> State) =
            when (action) {
                is Action.Sum -> sum(action.n)
            }

        // ...
    }

    // ...
}

And same for the CoroutineExecutor:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    // ...

    private class ExecutorImpl : CoroutineExecutor<Intent, Action, State, Msg, Nothing>() {
        override fun executeAction(action: Action, getState: () -> State) =
            when (action) {
                is Action.Sum -> sum(action.n)
            }

        // ...
    }

    // ...
}

The only thing is missing is we need to somehow trigger the Action. We need to pass a Bootstrapper to the StoreFactory. For such a simple case we can use ‘SimpleBootstrapper`:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    fun create(): CalculatorStore =
        object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create(
            name = "CounterStore",
            initialState = State(),
            bootstrapper = SimpleBootstrapper(Action.Sum(100)), // <-- Add this line
            executorFactory = ::ExecutorImpl,
            reducer = ReducerImpl
        ) {
        }

    // ...
}

The SimpleBootstrapper just dispatches the provided Actions. But sometimes we need more, e.g. do some background work.

Using ReaktiveBootstrapper from the mvikotlin-extensions-reaktive module:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    fun create(): CalculatorStore =
        object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create(
            name = "CounterStore",
            initialState = State(),
            bootstrapper = BootstrapperImpl(), // <-- Pass BootstrapperImpl to the StoreFactory
            executorFactory = ::ExecutorImpl,
            reducer = ReducerImpl
        ) {
        }

    private sealed interface Action {
        class SetValue(val value: Long): Action // <-- Use another Action
    }

    // ...

    private class BootstrapperImpl : ReaktiveBootstrapper<Action>() {
        override fun invoke() {
            singleFromFunction { (1L..1000000.toLong()).sum() }
                .subscribeOn(computationScheduler)
                .observeOn(mainScheduler)
                .subscribeScoped { dispatch(Action.SetValue(it)) }
        }
    }

    private class ExecutorImpl : ReaktiveExecutor<Intent, Action, State, Msg, Nothing>() {
        override fun executeAction(action: Action, getState: () -> State) =
            when (action) {
                is Action.SetValue -> dispatch(Msg.Value(action.value)) // <-- Handle the Action
            }

        // ...
    }

    // ...
}

⚠️ ReaktiveBootstrapper also implements DisposableScope, same as ReaktiveExecutor. So we can use subscribeScoped extension functions here as well.

Using CoroutineBootstrapper from the mvikotlin-extensions-coroutines module:

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {

    fun create(): CalculatorStore =
        object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create(
            name = "CounterStore",
            initialState = State(),
            bootstrapper = BootstrapperImpl(),
            executorFactory = ::ExecutorImpl,
            reducer = ReducerImpl
        ) {
        }

    private sealed interface Action {
        class SetValue(val value: Long): Action
    }

    // ...

    private class BootstrapperImpl : CoroutineBootstrapper<Action>() {
        override fun invoke() {
            scope.launch {
                val sum = withContext(Dispatchers.Default) { (1L..1000000.toLong()).sum() }
                dispatch(Action.SetValue(sum))
            }
        }
    }

    private class ExecutorImpl : CoroutineExecutor<Intent, Action, State, Msg, Nothing>() {
        override fun executeAction(action: Action, getState: () -> State) =
            when (action) {
                is Action.SetValue -> dispatch(Msg.Value(action.value))
            }

        // ...
    }

    // ...
}

⚠️ CoroutineBootstrapper also provides the CoroutineScope property named scope, same as CoroutineExecutor. So we can use it run asynchronous tasks.

Store DSL API

The approach demonstrated above is default, but may be considered verbose. MVIKotlin provides additional Store DSL API.

Reaktive way

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {
    fun create(): CalculatorStore =
        object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create<Intent, Action, Msg, State, Nothing>(
            name = "CounterStore",
            initialState = State(),
            bootstrapper = reaktiveBootstrapper {
                singleFromFunction { (1L..1000000.toLong()).sum() }
                    .subscribeOn(computationScheduler)
                    .observeOn(mainScheduler)
                    .subscribeScoped { // Use the DisposableScope for scoped subscriptions
                        dispatch(Action.SetValue(it)) // Dispatch an Action
                    }
            },
            executorFactory = reaktiveExecutorFactory {
                // Register a handler for Action.SetValue
                onAction<Action.SetValue> { action ->
                    dispatch(Msg.Value(action.value)) // Read the Action and dispatch a Message
                }

                // Register a handler for Intent.Increment
                onIntent<Intent.Increment> {
                    dispatch(Msg.Value(state.value + 1)) // Read the current state and dispatch a Message
                }

                onIntent<Intent.Decrement> { dispatch(Msg.Value(state.value - 1)) }

                onIntent<Intent.Sum> { intent ->
                    singleFromFunction { (1L..intent.n.toLong()).sum() }
                        .subscribeOn(computationScheduler)
                        .observeOn(mainScheduler)
                        .subscribeScoped { dispatch(Msg.Value(it)) } // Use the DisposableScope for scoped subscriptions
                }
            },
            reducer = { msg ->
                when (msg) {
                    is Msg.Value -> copy(value = msg.value)
                }
            }
        ) {
        }

    private sealed interface Action {
        // ...
    }

    private sealed interface Msg {
        // ...
    }
}

Coroutines way

internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {
    fun create(): CalculatorStore =
        object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create<Intent, Action, Msg, State, Nothing>(
            name = "CounterStore",
            initialState = State(),
            bootstrapper = coroutineBootstrapper {
                launch { // Launch a coroutine
                    val sum = withContext(Dispatchers.Default) { (1L..1000000.toLong()).sum() }
                    dispatch(Action.SetValue(sum)) // Dispatch an Action
                }
            },
            executorFactory = coroutineExecutorFactory {
                // Register a handler for Action.SetValue
                onAction<Action.SetValue> { action ->
                    dispatch(Msg.Value(action.value)) // Read the Action and dispatch a Message
                }

                // Register a handler for Intent.Increment
                onIntent<Intent.Increment> {
                    dispatch(Msg.Value(state.value + 1)) // Read the current state and dispatch a Message
                }

                onIntent<Intent.Decrement> { dispatch(Msg.Value(state.value - 1)) }

                onIntent<Intent.Sum> { intent ->
                    launch { // Launch a coroutine
                        val sum = withContext(Dispatchers.Default) { (1L..intent.n.toLong()).sum() }
                        dispatch(Msg.Value(sum))
                    }
                }
            },
            reducer = { msg ->
                when (msg) {
                    is Msg.Value -> copy(value = msg.value)
                }
            }
        ) {
        }

    private sealed interface Action {
        // ...
    }

    private sealed interface Msg {
        // ...
    }
}

Alternative way of creating a Store

If the amount of boilerplate code is still significant, there is an alternative way of creating a Store. Just get rid of the dedicated Store interface and put its content in top level.

internal sealed interface Intent {
    // ...
}

internal data class State(
    // ...
)

internal sealed interface Label {
    // ...
}

internal fun CalculatorStore(storeFactory: StoreFactory): Store<Intent, State, Label> =
    storeFactory.create<Intent, Action, Msg, State, Label>(
        name = "CounterStore",
        initialState = State(),
        bootstrapper = coroutineBootstrapper { // Or reaktiveBootstrapper
            // ...
        },
        executorFactory = coroutineExecutorFactory { // Or reaktiveExecutorFactory
            // ...
        },
        reducer = { msg ->
            // ...
        }
    )

private sealed interface Action {
    // ...
}

private sealed interface Msg {
    // ...
}
Overview Store View Binding and Lifecycle State preservation Logging Time travel