| 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:
- There are three generic parameters: input
Intentand outputStateandLabel. - The property named
statereturns the currentStateof theStore. - Can be instantiated (created) on any thread.
- Its
states(Observer<State>)method is used to subscribe forStateupdates. When subscribed it emits the currentStateof theStore. Can be called (subscribed) on any thread,Statesare emitted always on the main thread. - The
labels(Observer<Label>)method is used to subscribe forLabels. Can be called (subscribed) on any thread,Labelsare emitted always on the main thread. - The
accept(Intent)method supplies theStorewith theIntents, must be called only on the main thread. - The
init()method initializes theStoreand triggers theBootstrapperif applicable, must be called only on the main thread. - The
dispose()method disposes theStoreand cancels all its async operations, must be called only on the main thread.
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:
Store.states- returnsBehaviorObservableof typeState.Store.labels- returnsObservableof typeLabel.
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.states- returnsFlowof typeState.Store.stateFlow- returnsStateFlowof typeState.Store.labels- returnsFlowof typeLabel.
Store structure
Every Store has up to three components: Bootstrapper, Executor and Reducer. Here is the diagram of how they are connected:

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
Bootstrappersare stateful and so can not beobjects (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
Executorsare stateful and so can not beobjects (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
Executorsare stateful and so can not beobjects (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:
- DefaultStoreFactory creates a default implementation of
Storeand is provided by themvikotlin-mainmodule. - LoggingStoreFactory wraps another
StoreFactoryand adds logging, it’s provided by themvikotlin-loggingmodule. - TimeTravelStoreFactory is provided by the
mvikotlin-timetravelmodule, it creates aStorewith time travel functionality.
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:
mvisi- adds a skeletonStoreinterface.mvisr- adds a skeletonStorefactory with Reaktive.mvisc- adds a skeletonStorefactory with coroutines.mvisfr- adds skeletons forStoreinterface and factory with Reaktive.mvisfc- adds skeletons forStoreinterface and factory with coroutines.
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:
- ReaktiveExecutor - this implementation is based on the Reaktive library and is provided by
mvikotlin-extensions-reaktivemodule - CoroutineExecutor - this implementation is based on the Coroutines library and is provided by
mvikotlin-extensions-coroutinesmodule
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.
⚠️
ReaktiveExecutorimplements 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 theStore(and so theExecutor) 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.
⚠️
CoroutineExecutorprovides theCoroutineScopeproperty namedscope, which can be used to run asynchronous tasks. The scope usesDispatchers.Maindispatcher by default, which can be overriden by passing differentCoroutineContextto theCoroutineExecutorconstructor. The scope is automatically cancelled when theStoreis 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
Labelis published straight fromExecutor.executeActionmethod, theStoremay have no subscribers yet. This may happen if theStoreis still being created, and the initialization is still in progress. Prefer manualStoreinitialization 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
executorFactoryshould return a new instance of theExecutorevery 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
}
// ...
}
// ...
}
⚠️
ReaktiveBootstrapperalso implementsDisposableScope, same asReaktiveExecutor. So we can usesubscribeScopedextension 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))
}
// ...
}
// ...
}
⚠️
CoroutineBootstrapperalso provides theCoroutineScopeproperty namedscope, same asCoroutineExecutor. 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 |