# FlowMVI
> Architecture Framework for Kotlin. Reuse every line of code. Handle all errors automatically. No boilerplate. Analytics, metrics, debugging in 3 lines. 50+ features.
Complete offline documentation bundle for FlowMVI.
This file contains all documentation content in a single document following the llmstxt.org standard.
**Format**: Markdown with code examples
**Languages**: Kotlin, Java
**Platforms**: Android, iOS, Desktop, Web
All code examples are Apache 2.0 licensed unless otherwise noted.
## Get Started with FlowMVI
In this guide, you can learn everything you need to get started and we'll build a feature with UI in 10 minutes.
## Step 0: Understand the primary concepts
First of all, here's how the library works:
- **Stores** are classes that _respond_ to events, called **Intents**, and _update_ their **State**.
- Responding to Intents is called _reducing_.
- You _add functionality_ to Stores using **Plugins**, which form a **Pipeline**.
- Clients _subscribe_ to Stores to _render_ their **State** and _consume_ side-effects, called **Actions**.
- States, Intents, and Actions together form a **Contract**.
Show the diagram
## Step 1: Configure the library
### 1.1: Add dependencies 
```toml
[versions]
flowmvi = "< Badge above ππ» >"
[dependencies]
# Core KMP module
flowmvi-core = { module = "pro.respawn.flowmvi:core", version.ref = "flowmvi" }
# Test DSL
flowmvi-test = { module = "pro.respawn.flowmvi:test", version.ref = "flowmvi" }
# Compose multiplatform
flowmvi-compose = { module = "pro.respawn.flowmvi:compose", version.ref = "flowmvi" }
# Android (common + view-based)
flowmvi-android = { module = "pro.respawn.flowmvi:android", version.ref = "flowmvi" }
# Multiplatform state preservation
flowmvi-savedstate = { module = "pro.respawn.flowmvi:savedstate", version.ref = "flowmvi" }
# Performance metrics collection
flowmvi-metrics = { module = "pro.respawn.flowmvi:metrics", version.ref = "flowmvi" }
# Remote debugging client
flowmvi-debugger = { module = "pro.respawn.flowmvi:debugger-plugin", version.ref = "flowmvi" }
# Essenty (Decompose) integration
flowmvi-essenty = { module = "pro.respawn.flowmvi:essenty", version.ref = "flowmvi" }
flowmvi-essenty-compose = { module = "pro.respawn.flowmvi:essenty-compose", version.ref = "flowmvi" }
```
```kotlin
dependencies {
val flowmvi = "< Badge above ππ» >"
// Core KMP module
commonMainImplementation("pro.respawn.flowmvi:core:$flowmvi")
// compose multiplatform
commonMainImplementation("pro.respawn.flowmvi:compose:$flowmvi")
// saving and restoring state
commonMainImplementation("pro.respawn.flowmvi:savedstate:$flowmvi")
// metrics collection & export
commonMainImplementation("pro.respawn.flowmvi:metrics:$flowmvi")
// essenty integration
commonMainImplementation("pro.respawn.flowmvi:essenty:$flowmvi")
commonMainImplementation("pro.respawn.flowmvi:essenty-compose:$flowmvi")
// testing DSL
commonTestImplementation("pro.respawn.flowmvi:test:$flowmvi")
// android integration
androidMainImplementation("pro.respawn.flowmvi:android:$flowmvi")
// remote debugging client (use on debug only)
debugImplementation("pro.respawn.flowmvi:debugger-plugin:$flowmvi")
}
```
### 1.2: Set up the IDE
FMVI comes with an IDE plugin that provides lint checks and templates to generate code. Consider installing it to make
the amount of boilerplate you write minimal.
## Step 2: Choose your style
FlowMVI supports both MVI (strict model-driven) and MVVM+ (functional, lambda-driven) styles.
- **Model-driven** means that you create an `MVIIntent` subclass for every event that happens, and the store
decides how to handle it. Model-driven intents are recommended to take full advantage of Plugins and are used in the
examples below.
- **Functional** means that you invoke functions which contain your business logic, and then send the
logic for processing to the store.
See this section if you can't decide
It's preferable to choose one style and use it throughout your project.
Each style has its own pros and cons, so choosing can be hard. So please consider the following comparison:
### MVI style:
| Pros π | Cons π |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Greater separation of concerns as intent handling logic is strictly contained in the store's scope | Boilerplatish: some intents will need to be duplicated for multiple screens, resulting in some amount of copy-paste |
| Verbose and readable - easily understand which intent does what judging by the contract | Hard to navigate in the IDE. You have to jump twice: first to the declaration, and then to usage, to see the code of the intent |
| Intents can be decomposed into sealed families, subclassed, delegated, have properties or functions | Class explosion - class for every event may result in 50+ model classes per screen easily |
| Intents can be re-sent inside the store - by sending an intent while handling another intent | Sealed classes work worse for some platforms, for example, in Swift, Enums are not used and names are mangled |
### MVVM+ style:
| Pros π | Cons π |
| :------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------- |
| Elegant syntax - open a lambda block and write your logic there. Store's code remains clean | You have to use `ImmutableStore` interface to not leak the store's context |
| Easily navigate to and see what an intent does in one click | Lambdas are less performant than regular intents |
| Easier to support on other platforms if handled correctly (not exposing store's logic in platform code) | Some Plugins will become useless, such as logging/time travel/analytics |
| Get rid of all Intent classes entirely, avoid class explosion | Intents cannot be composed, delegated and organized into families |
If you decide to use the MVVM+ style, then your Intents will look like this:
```kotlin
fun onItemClick(item: Item) = store.intent {
updateState {
copy(selectedItem = item)
}
}
```
- If you use MVVM+ style, you **must** install the `reduceLambdas` Plugin to make the store handle your intents.
Plugins are explained below.
- With MVVM+ style, consider using the `ImmutableStore` and `ImmutableContainer` interfaces that won't let
external code send intents. This will prevent leaking the context of the Store to subscribers.
## Step 3: Describe your Contract
Type `fmvim` in your editor to let the IDE Plugin generate a Contract for you:
```kotlin
// States
sealed interface CounterState : MVIState {
data object Loading : CounterState
data class Error(val e: Exception) : CounterState
data class DisplayingCounter(val counter: Int) : CounterState
}
// MVI Style Intents
sealed interface CounterIntent : MVIIntent {
data object ClickedNext : CounterIntent
data class ChangedCounter(val value: Int) : CounterIntent
data class GrantedPermission(val granted: Boolean, val permission: String) : CounterIntent
}
// MVVM+ Style Intents
typealias CounterIntent = LambdaIntent
// Side-effects
sealed interface CounterAction : MVIAction {
data class ShowMessage(val message: String) : CounterAction
}
```
- If your store does not have a `State`, you can use an `EmptyState` object provided by the library.
- If your store does not have side effects, use `Nothing` in place of the side-effect type.
Click if you need help defining a contract
Describing the contract first makes building the logic easier because this helps make it declarative.
To define your contract, ask yourself the following:
1. What can be shown at what times? Can the page be empty? Can it be loading? Can errors happen? -
this will define your state family.
2. What elements can be shown on this screen, for each state? - these will define your state properties.
3. What can the user do on this screen? What can happen in the system? - these will be your Intents.
4. In response to given intents, what one-time events may happen? - these are Actions. Keep in mind that side-effects
are not the best way to manage your business logic. Try to make your UI/platform logic stateful first, and resort
to side-effects only if a third-party API you're working with is imperative (such as Android SDK or View system).
- The `MVIState` is what should be displayed or used by the UI layer. Whenever the state changes,
update **all** of your UI with the current properties of the state.
- Do **not** make your state mutable. Because FlowMVI uses `StateFlow`s under the hood, your state changes
**won't be reflected** if you mutate your state using `var`s or
by using mutable properties such as `MutableList`s.
Use `copy()` of the data classes to mutate your state instead. Even if you use `List`s as the value type,
for example, make sure those are **new** lists and not just `MutableList`s that were upcasted.
- It's okay to copy the state often, modern devices can handle a few garbage collections.
- The `MVIIntent` is an action that the user or the subscriber takes, for example: clicks, system broadcasts and dialog
button presses.
- The `MVIAction` is a one-off event that should happen in the UI or that the subscriber should handle.
- Examples include snackbars, popup messages, sounds and so on.
- Prefer using state instead of events if you are able to always know the
outcome. [Read more here](https://proandroiddev.com/viewmodel-events-as-state-are-an-antipattern-35ff4fbc6fb6).
- Do not confuse States with Actions! Actions are **one-off, "fire and forget" events** that cannot be tracked after being sent.
- Actions are **sent and received sequentially**.
- Intents are sent **to** the Store. Actions are sent **from** the Store.
- Actions are not strictly guaranteed to be received by the subscriber, so do not use them for crucial elements of
the logic.
## Step 4: Create your Store
You'll likely want to:
1. Provide some dependencies for the Store to use, and
2. Create additional functions instead of just putting everything into the Store's builder.
The best way to do this is to create a class that acts as a simple wrapper for your Store. By convention, it is
usually named `Container`.
Generate a Container using the IDE plugin by typing `fmvic`:
```kotlin
private typealias Ctx = PipelineContext
class CounterContainer(
private val repo: CounterRepository,
) : Container {
override val store = store(initial = CounterState.Loading) {
}
// custom function
private fun Ctx.produceState(timer: Int) = updateState { DisplayingCounter(timer) }
}
```
Use `lazyStore` function to create a Store lazily if you don't plan to use it right away.
## Step 5: Configure your store
To get started, you only need to set the `debuggable` parameter - it enables a lot of additional
features like logging and validations.
Call `configure` inside your Store builder to change its settings:
```kotlin
val store = store(initial = CounterState.Loading) {
configure {
debuggable = BuildFlags.debuggable
name = "CounterStore"
}
}
```
See this section for full explanation of the properties
Here are all of the configurable settings with defaults assigned:
```kotlin
configure {
debuggable = false
name = null
parallelIntents = false
coroutineContext = EmptyCoroutineContext
actionShareBehavior = ActionShareBehavior.Distribute()
onOverflow = BufferOverflow.DROP_OLDEST
intentCapacity = Channel.UNLIMITED
stateStrategy = StateStrategy.Atomic(reentrant = true)
allowIdleSubscriptions = false
logger = if (debuggable) PlatformStoreLogger else NoOpStoreLogger
verifyPlugins = debuggable
}
```
- `debuggable` - Setting this to `true` enables additional store validations and debug logging. The store will check
your subscription events, launches/stops, and Plugins for validity, as well as print logs to the system console.
- `name` - Set the future name of the store. Needed for debug, logging, comparing and injecting stores, analytics.
- `parallelIntents` - Declare that intents must be processed in parallel. Intents may still be dropped according to the
`onOverflow` param.
- `coroutineContext` - A coroutine context override for the store. This context will be merged with the one the store
was launched with (e.g. `viewModelScope`). All store operations will be launched in that context by default.
- `actionShareBehavior` - Define how the store handles and sends actions. Choose one of the following:
- `Distribute` - send side effects in a fan-out FIFO fashion to one subscriber at a time (default).
- `Share` - share side effects between subscribers using a `SharedFlow`. If an event is sent and there are no
subscribers, the event **will be lost!**
- `Restrict` - Allow only **one** subscription event per whole lifecycle of the store. If you want to subscribe
again, you will have to re-create the store.
- `Disable` - Disable side effects.
- `onOverflow` - Designate behavior for when the store's intent queue overflows. Choose from:
- `BufferOverflow.SUSPEND` - Suspend on buffer overflow.
- `BufferOverflow.DROP_OLDEST` - Drop **the oldest** value in the buffer on overflow, add the new value to the
buffer, do not
suspend (default).
- `BufferOverflow.DROP_LATEST` - Drop **the latest** value that is being added to the buffer right now on buffer
overflow (so that
buffer contents stay the same), do not suspend.
- `intentCapacity` - Designate the maximum capacity of store's intent queue. This should be either:
- A positive value of the buffer size
- `Channel.UNLIMITED` - unlimited buffer (default)
- `Channel.CONFLATED` - A buffer of 1
- `Channel.RENDEZVOUS` - Zero buffer (all events not ready to be processed are dropped)
- `Channel.BUFFERED` - Default system buffer capacity
- `stateStrategy` - Strategy for serializing state transactions. Choose one of the following:
- `Atomic(reentrant = false)` - Enables transaction serialization for state updates, making state updates atomic and
suspendable. Synchronizes state updates, allowing only **one** client to read and/or update the state at a time.
All other clients that attempt to get the state will wait in a FIFO queue and suspend the parent coroutine. For
one-time usage of non-atomic updates, see `updateStateImmediate`. Recommended for most cases.
- `Atomic(reentrant = true)` - Same as above, but allows nested state updates
without causing a deadlock, like this: `updateState { updateState { } }`. This strategy is 15x slower than other
options, but still negligible for managing UI and other non-performance-critical tasks. This is the default.
- `Immediate` - 2 times faster than atomic with no reentrancy, but provides no state consistency guarantees
and no thread-safety. Equivalent to always using `updateStateImmediate`.
- `allowIdleSubscriptions` - A flag to indicate that clients may subscribe to this store even while it is not started.
If you intend to stop and restart your store while the subscribers are present, set this to `true`. By default, will
use the opposite value of the `debuggable` parameter (`true` on production).
- `logger` - An instance of `StoreLogger` to use for logging events. By default, the value is chosen based on
the `debuggable` parameter:
- `PlatformStoreLogger` that logs to the primary log stream of the system (e.g. Logcat on Android).
- `NoOpStoreLogger` - if `debuggable` is false, logs will not be printed.
## Step 6: Install Plugins
**Everything** in FlowMVI is a Plugin. This includes handling errors and even **reducing intents**.
One Plugin almost every Store needs is the `reduce` Plugin. Install it when building your store:
```kotlin
override val store = store(Loading) {
configure { /* ... */ }
reduce { intent ->
when (intent) {
is ChangedCounter -> updateState<_, DisplayingCounter> {
copy(intent.newValue)
}
}
}
}
```
Every Plugin method has a special receiver called `PipelineContext`. It's like an "environment" the Store
runs in, and gives you access to everything you need:
- `updateState { }` - update the state of the store using the return value. Code in the block is thread-safe.
- `withState { }` - grab the state thread-safely and use it, but do not change it.
- `action()` - send a side-effect to subscribers
- `intent()` - re-send and delegate to another intent
- `config` - use the store's configuration, for example, to issue log calls:
`log { "initial state = ${config.initial}" }`. It will only print if `debuggable` is `true` by default.
There are many other pre-made Plugins. Check out the [Plugins](/plugins/prebuilt.md) page to learn about all of them.
## Step 7: Start your store
Provide a coroutine scope with a lifecycle that matches the duration your Store should be accepting Intents
and running background jobs.
- On [Android](/integrations/android.md), this will likely be a `viewModelScope`.
- On Desktop w/Compose, you can use `rememberCoroutineScope()`.
- On iOS, provide a scope manually or through a library.
#### Automatically:
```kotlin
fun counterStore(scope: CoroutineScope) = store(initial = Loading, scope = scope) { /* ... */ }
```
#### Manually
```kotlin
val scope = CoroutineScope()
val store = counterStore()
// start
val lifecycle = store.start(scope)
// stop
scope.cancel()
// or to keep the scope alive
lifecycle.close()
```
:::warning[Don't forget to start your Store]
Store will do **nothing** unless it is started using the
`start(scope: CoroutineScope)` function or a scope is provided as a parameter to the builder.
:::
:::tip
Store can be started and stopped as many times as you want. It will clean up everything except
its state after itself.
:::
## Step 8: Subscribe to your Store
The way you do this varies a lot based on what you will use the Store for and your app's UI framework, if any.
In this example, we'll subscribe using Compose to a Store made for managing UI state.
Type `fmvis` to generate a new composable screen using the IDE plugin:
```kotlin
@Composable
fun CounterScreen(
container: CounterContainer = DI.inject(),
) = with(container.store) {
val state by subscribe { action ->
when (action) {
is ShowMessage -> {
/* ... */
}
}
}
CounterScreenContent(state)
}
@Composable
private fun IntentReceiver.CounterScreenContent(state: DisplayingCounterState) {
/* ... */
}
```
- To learn more about FMVI in Compose, see [this guide](/integrations/compose.md)
- To subscribe using Android Views, see the [android guide](/integrations/android.md)
## Next Steps
That's it! You have set up FlowMVI in ~100 lines of code.
Now you can start using the features of the library to write scalable business logic with Plugins.
Continue learning by reading these articles:
1. Learn how to [install](/plugins/prebuilt.md) and [create](/plugins/custom.md) Plugins
2. Learn how to [manage](/state/statemanagement.md) and [persist](/state/savedstate.md) application State.
3. Learn how to use FlowMVI with [compose](/integrations/compose.md)
4. Set up [remote debugging](/plugins/debugging.md) and [Dependency Injection](/integrations/di.md)
5. Learn how to use FlowMVI on [Android](/integrations/android.md)
6. Get answers to common [questions](/misc/FAQ.md)
7. Explore the [Resources](/misc/resources.md) for code examples, articles, videos & more.
---
## FlowMVI

[](https://github.com/respawn-app/FlowMVI/actions/workflows/ci.yml)




[](https://androidweekly.net/issues/issue-563/)
[](https://kotlinlang.slack.com/messages/flowmvi/)
[](https://deepwiki.com/respawn-app/FlowMVI)
![badge][badge-android] ![badge][badge-jvm] ![badge][badge-js] ![badge][badge-nodejs] ![badge][badge-linux] ![badge][badge-windows] ![badge][badge-ios] ![badge][badge-mac] ![badge][badge-watchos] ![badge][badge-tvos] ![badge][badge-wasm]
#### FlowMVI is a Kotlin Multiplatform Architectural Framework.
It **adds a plug-in system** to your code that helps prevent crashes, handle errors,
split responsibilities, reuse code, collect analytics, debug & log operations, monitor & improve performance,
achieve thread-safety, save state, manage background jobs, and more.
## β‘οΈ Quickstart:
* Get Started in 10 mins:
[](https://opensource.respawn.pro/FlowMVI/quickstart)
* Latest version:
[](https://central.sonatype.com/namespace/pro.respawn.flowmvi)
* API Docs:
[](https://opensource.respawn.pro/FlowMVI/javadocs/index.html)
* Sample App + Showcase (Web):
[](https://opensource.respawn.pro/FlowMVI/sample/)
* Ask questions on
[](https://kotlinlang.slack.com/messages/flowmvi/)
Version catalogs
```toml
[versions]
flowmvi = "< Badge above ππ» >"
[dependencies]
# Core KMP module
flowmvi-core = { module = "pro.respawn.flowmvi:core", version.ref = "flowmvi" }
# Test DSL
flowmvi-test = { module = "pro.respawn.flowmvi:test", version.ref = "flowmvi" }
# Compose multiplatform
flowmvi-compose = { module = "pro.respawn.flowmvi:compose", version.ref = "flowmvi" }
# Android (common + view-based)
flowmvi-android = { module = "pro.respawn.flowmvi:android", version.ref = "flowmvi" }
# Multiplatform state preservation
flowmvi-savedstate = { module = "pro.respawn.flowmvi:savedstate", version.ref = "flowmvi" }
# Performance metrics collection
flowmvi-metrics = { module = "pro.respawn.flowmvi:metrics", version.ref = "flowmvi" }
# Remote debugging client
flowmvi-debugger-client = { module = "pro.respawn.flowmvi:debugger-plugin", version.ref = "flowmvi" }
# Essenty (Decompose) integration
flowmvi-essenty = { module = "pro.respawn.flowmvi:essenty", version.ref = "flowmvi" }
flowmvi-essenty-compose = { module = "pro.respawn.flowmvi:essenty-compose", version.ref = "flowmvi" }
```
Gradle DSL
```kotlin
dependencies {
val flowmvi = "< Badge above ππ» >"
// Core KMP module
commonMainImplementation("pro.respawn.flowmvi:core:$flowmvi")
// compose multiplatform
commonMainImplementation("pro.respawn.flowmvi:compose:$flowmvi")
// saving and restoring state
commonMainImplementation("pro.respawn.flowmvi:savedstate:$flowmvi")
// metrics collection & export
commonMainImplementation("pro.respawn.flowmvi:metrics:$flowmvi")
// essenty integration
commonMainImplementation("pro.respawn.flowmvi:essenty:$flowmvi")
commonMainImplementation("pro.respawn.flowmvi:essenty-compose:$flowmvi")
// testing DSL
commonTestImplementation("pro.respawn.flowmvi:test:$flowmvi")
// android integration
androidMainImplementation("pro.respawn.flowmvi:android:$flowmvi")
// remote debugging client
androidDebugImplementation("pro.respawn.flowmvi:debugger-plugin:$flowmvi")
}
```
## π Why FlowMVI?
Usually architecture frameworks mean boilerplate, restrictions, and support difficulty for marginal benefits
of "clean code". FlowMVI does not dictate what your code should do or look like.
Instead, this library focuses on **building a supporting infrastructure** to enable new possibilities for your app.
Here's what you get:
* Powerful plug-in system to **reuse any business logic** you desire.
Write your auth, error handling, analytics, logging, configuration, and any other code **once**
and forget about it, focusing on more important things instead.
* Automatically **recover from exceptions**, prevent crashes, and report them to analytics.
* Automatically collect and view logs: forget about `Log.e("asdf")` sprinkling.
* **Collect 50+ performance metrics** with Prometheus, Grafana, OpenTelemetry export and 5 lines of setup.
* Manage concurrent, long-running **background jobs** with complete thread-safety.
* Debounce, retry, batch, throttle, conflate, **intercept any operations** automatically
* **Compress, persist, and restore state** automatically on any platform
* Create **compile-time safe state machines** with a readable DSL. Forget about casts, inconsistent states, and `null`s
* Share, distribute, disable, intercept, safely **manage side-effects**
* Build fully **async, reactive and parallel apps** - with no manual thread synchronization required!
* Write **simple, familiar MVVM+** code or follow MVI/Redux - no limits or requirements
* Build restartable, reusable business logic components with **no external dependencies** or dedicated lifecycles
* No base classes, complicated abstractions, or factories of factories - write **simple, declarative logic** using a DSL
* Automatic multiplatform system **lifecycle handling**
* First class, one-liner **Compose Multiplatform support** with Previews and UI tests.
* Integrates with [Decompose](https://github.com/arkivanov/Decompose), Koin, Kodein, androidx.navigation, Nav3, and more
* Dedicated **IDE Plugin for debugging and codegen** and app for Windows, Linux, macOS
* The core **library has no dependencies** - just coroutines
* Extensively covered by **350+ tests**
* **Minimal performance overhead**, equal to using a simple Channel, with regular benchmarking
* **Test any business logic** using clean, declarative DSL
* Learn more by exploring the [sample app](https://opensource.respawn.pro/FlowMVI/sample/) in your browser
* 10 minutes to try by following the [Quickstart Guide](https://opensource.respawn.pro/FlowMVI/quickstart).
## π Show me the code!
Here is an example of your new workflow:
### 1. Define a Contract:
```kotlin
sealed interface State : MVIState {
data object Loading : State
data class Error(val e: Exception) : State
data class Content(val user: User?) : State
}
sealed interface Intent : MVIIntent {
data object ClickedSignOut : Intent
}
sealed interface Action : MVIAction { // optional side-effect
data class ShowMessage(val message: String) : Action
}
```
### 2. Declare your business logic:
```kotlin
val authStore = store(initial = State.Loading, coroutineScope) {
recover { e: Exception -> // handle errors
updateState { State.Error(e) }
null
}
init { // load data
updateState {
State.Content(user = repository.loadUser())
}
}
reduce { intent: Intent -> // respond to events
when (intent) {
is ClickedSignOut -> updateState {
action(ShowMessage("Bye!"))
copy(user = null)
}
}
}
}
```
FlowMVI lets you scale your app in a way that does not increase complexity.
Adding a new feature is as simple as calling a function.
## Extend your logic with Plugins
Powerful DSL allows you to hook into various events and amend any part of your logic:
```kotlin
fun analyticsPlugin(analytics: Analytics) = plugin {
onStart {
analytics.logScreenView(config.name) // name of the screen
}
onIntent { intent ->
analytics.logUserAction(intent.name)
}
onException { e ->
analytics.logError(e)
}
onSubscribe {
analytics.logEngagementStart(config.name)
}
onUnsubscribe {
analytics.logEngagementEnd(config.name)
}
onStop {
analytics.logScreenLeave(config.name)
}
}
```
Never write analytics, debugging, logging, or state persistence code again.
## Compose Multiplatform
Using FlowMVI with Compose is a matter of one line of code:
```kotlin
@Composable
fun AuthScreen() {
// subscribe based on system lifecycle - on any platform
val state by authStore.subscribe
when (state) {
is Content -> {
Button(onClick = { store.intent(ClickedSignOut) }) {
Text("Sign Out")
}
}
}
}
```
Enjoy testable UI and free `@Preview`s.
## Testing DSL
Bundled Test Harness with minimal verbosity:
### Test Stores
```kotlin
authStore.subscribeAndTest {
// turbine + kotest example
intent(ClickedSignOut)
states.test {
awaitItem() shouldBe Content(user = null)
}
actions.test {
awaitItem() shouldBe ShowMessage("Bye!")
}
}
```
### Test plugins
```kotlin
val timer = Timer()
timerPlugin(timer).test(Loading) {
onStart()
// time travel keeps track of all plugin operations for you
assert(timeTravel.starts == 1)
assert(state is DisplayingCounter)
assert(timer.isStarted)
onStop(null)
assert(!timer.isStarted)
}
```
Finally stop writing UI tests and replace them with unit tests.
## Debugger IDE Plugin + App
IDE plugin generates code and lets you debug and control your app remotely:
[](https://plugins.jetbrains.com/plugin/25766-flowmvi)
## People love the library:
## Ready to try?
Begin by reading the [Quickstart Guide](https://opensource.respawn.pro/FlowMVI/quickstart).
----
## License
```
Copyright 2022-2026 Respawn Team and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
[badge-android]: https://img.shields.io/badge/-android-6EDB8D.svg?style=flat
[badge-android-native]: https://img.shields.io/badge/support-[AndroidNative]-6EDB8D.svg?style=flat
[badge-jvm]: https://img.shields.io/badge/-jvm-DB413D.svg?style=flat
[badge-js]: https://img.shields.io/badge/-js-F8DB5D.svg?style=flat
[badge-js-ir]: https://img.shields.io/badge/support-[IR]-AAC4E0.svg?style=flat
[badge-nodejs]: https://img.shields.io/badge/-nodejs-68a063.svg?style=flat
[badge-linux]: https://img.shields.io/badge/-linux-2D3F6C.svg?style=flat
[badge-windows]: https://img.shields.io/badge/-windows-4D76CD.svg?style=flat
[badge-wasm]: https://img.shields.io/badge/-wasm-624FE8.svg?style=flat
[badge-apple-silicon]: https://img.shields.io/badge/support-[AppleSilicon]-43BBFF.svg?style=flat
[badge-ios]: https://img.shields.io/badge/-ios-CDCDCD.svg?style=flat
[badge-mac]: https://img.shields.io/badge/-macos-111111.svg?style=flat
[badge-watchos]: https://img.shields.io/badge/-watchos-C0C0C0.svg?style=flat
[badge-tvos]: https://img.shields.io/badge/-tvos-808080.svg?style=flat
---
## Use FlowMVI with AI Agents
## LLM-friendly docs
We publish `llms.txt` and `llms-full.txt` at the documentation root so agents can index the full docs quickly.
```bash
curl -L https://opensource.respawn.pro/FlowMVI/llms.txt
curl -L https://opensource.respawn.pro/FlowMVI/llms-full.txt
```
You can also fetch any doc page as markdown by appending `.md` to its URL, which makes `curl`-based discovery easy:
```bash
curl -L https://opensource.respawn.pro/FlowMVI/quickstart.md
```
## Claude Code plugin
Install the FlowMVI Claude Code plugin from the Respawn marketplace:
```text
/plugin marketplace add respawn-app/claude-plugin-marketplace
/plugin install flowmvi@respawn-tools
```
## Codex skill
Install the FlowMVI Codex skill with the `skill-installer` skill:
```text
$skill-installer install https://github.com/respawn-app/FlowMVI/tree/main/skills/flowmvi
```
Restart Codex after installation to pick up the new skill.
## Manual use (no harness)
Download the `skills/flowmvi` folder and place it in your repository, then add to your `AGENTS.md`:
```
When working with FlowMVI stores/containers, plugin pipelines, composing stores, decorators, or authoring plugins, read
`skills/flowmvi/SKILL.md` to learn how to use the framework.
```
---
## Use FlowMVI with Android
There are multiple options on how to organize your code when working with Android.
The choice depends on your project's specific needs and each option has certain tradeoffs.
## ViewModel
### Direct ViewModels
The simplest way to organize code is to implement `Container` in your ViewModel.
You are not required to implement any interfaces, however. They are only served as markers/nice dsl providers.
Example that uses models from [quickstart](../quickstart.md) and MVVM+ style.
This example is also fully implemented in the sample app.
```kotlin
class CounterViewModel(
repo: CounterRepository,
handle: SavedStateHandle,
) : ViewModel(), ImmutableContainer {
// the store is lazy here, which is good for performance if you use other properties of the VM.
// if you don't want a lazy store, use the regular store() function here
override val store by lazyStore(
initial = Loading,
scope = viewModelScope,
) {
configure {
debuggable = BuildConfig.DEBUG
}
enableLogging()
parcelizeState(handle)
/* ... everything else ... */
reduceLambdas() // <-- don't forget that lambdas must still be reduced
}
fun onClickCounter() = store.intent {
action(ShowCounterIncrementedMessage)
updateState {
copy(counter = counter + 1)
}
}
}
```
Prefer to extend `ImmutableContainer` as that will hide the `intent` function from outside code, otherwise you'll leak
the `PipelineContext` of the store to subscribers.
:::info
The upside of this approach is that it's easier to implement and use some navigation-specific features
like `savedState` (you can still use them for KMP though)
The downside is that you lose KMP compatibility. If you have plans to make your ViewModels multiplatform,
it is advised to use the delegated approach instead, which is only slightly more verbose.
:::
### Delegated ViewModels
A slightly more advanced approach would be to avoid subclassing ViewModels altogether and using `ContainerViewModel`
that delegates to the Store.
This is a more robust and multiplatform-friendly approach that is slightly more boilerplatish, but does not require you
to subclass ViewModels.
The only caveat is injecting your `Container` into an instance of `StoreViewModel`, and then injecting the
`StoreViewModel` correctly. The implementation varies based on which DI framework you will be using, with some examples
are provided in the [DI Guide](/integrations/di.md)
## View Integration
For a View-based project, subscribe in an appropriate lifecycle callback and create two functions to render states
and consume actions.
* Subscribe in `Fragment.onViewCreated` or `Activity.onCreate`. The library will handle the lifecycle for you.
* Make sure your `render` function is idempotent, and `consume` function does not loop itself with intents.
* Always update **all views** in `render`, for **any state change**, to circumvent the problems of old-school stateful
view-based Android API.
```kotlin
class CounterFragment : Fragment() {
private val binding by viewBinding()
private val store: CounterContainer by container() // see DI guide for implementation
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribe(container, ::consume, ::render)
with(binding) {
tvCounter.setOnClickListener(store::onClickCounter) // let's say we are using MVVM+ style.
}
}
private fun render(state: CounterState): Unit = with(binding) {
with(state) {
tvCounter.text = counter.toString()
/* ... update ALL views! ... */
}
}
private fun consume(action: CounterAction): Unit = when (action) {
is ShowMessage -> Snackbar.make(binding.root, action.message, Snackbar.LENGTH_SHORT).show()
}
}
```
---
## Compose Integration
## Step 1: Add Dependencies

```toml
[versions]
flowmvi = "< Badge above ππ» >"
[dependencies]
flowmvi-compose = { module = "pro.respawn.flowmvi:compose", version.ref = "flowmvi" }
```
## Step 2: Configure the Compiler
Set up stability definitions for your project:
/project_root/stability_definitions.txt
```text
pro.respawn.flowmvi.api.MVIIntent
pro.respawn.flowmvi.api.MVIState
pro.respawn.flowmvi.api.MVIAction
pro.respawn.flowmvi.api.Store
pro.respawn.flowmvi.api.Container
pro.respawn.flowmvi.api.ImmutableStore
pro.respawn.flowmvi.dsl.LambdaIntent
pro.respawn.flowmvi.api.SubscriberLifecycle
pro.respawn.flowmvi.api.IntentReceiver
```
Then configure compose compiler to account for the definitions in your feature's `build.gradle.kts`:
/feature-module/build.gradle.kts
```kotlin
composeCompiler {
stabilityConfigurationFiles.add(rootProject.layout.projectDirectory.file("stability_definitions.txt"))
}
```
Now the states/intents you create will be stable in compose. Immutability of these classes is already required by the
library, so this will ensure you get the best performance. See the project's gradle configuration if you want to learn
how to set compose compiler configuration globally and/or in gradle conventions.
## Step 3: Subscribe to Stores
:::warning
Compose does not play well with MVVM+ style because of the instability of the `LambdaIntent` and `ViewModel` classes.
It is discouraged to use Lambda intents with Compose as that will not only leak the context of the store but
also degrade performance, also forcing you to pass tons of function references as parameters.
:::
Subscribing to a store is as simple as calling `subscribe()`
```kotlin
@Composable
fun CounterScreen(
container: CounterContainer,
) = with(container.store) {
val state by subscribe { action ->
when (action) {
is ShowMessage -> {
/* ... */
}
}
}
CounterScreenContent(state)
}
```
Under the hood, the `subscribe` function will efficiently subscribe to the store and
use the composition scope to process your events. Event processing will stop when the UI is no longer visible (by
default). When the UI is visible again, the function will re-subscribe. Your composable will recompose when the state
changes. By default, the function will use your system's default lifecycle, provided by the
compose `LocalLifecycleOwner`. If you are using a custom lifecycle implementation e.g. provided by the navigation
library, you can use that lifecycle by providing it as a `LocalSubscriberLifecycle` composition local or passing it as
a parameter to the `subscribe` function.
Use the lambda parameter of `subscribe` to subscribe to `MVIActions`. Those will be processed as they arrive and
the `consume` lambda will **suspend** until an action is processed. Use a receiver coroutine scope to
launch new coroutines that will parallelize your flow (e.g. for snackbars).
## Step 4: Create Pure UI Composables
A best practice is to make your state handling (UI redraw composable) a pure function and extract it to a separate
Composable such as `ScreenContent(state: ScreenState)` to keep your `*Screen` function clean, as shown below.
It will also enable smart-casting by the compiler make UI tests super easy. If you want to send `MVIIntent`s from a
nested composable, just use `IntentReceiver` as a context or pass a function reference:
```kotlin
@Composable
private fun IntentReceiver.CounterScreenContent(state: CounterState) {
when (state) {
is DisplayingCounter -> {
Button(onClick = { intent(ClickedCounter) }) { // intent() available from the receiver parameter
Text("Counter: ${state.counter}")
}
}
/* ... */
}
}
```
Now this function cannot be called outside of the required store's area of responsibility.
You can also subclass your `Intent` class by target state to make it impossible at compilation time to send an intent
for an incorrect state:
```kotlin
sealed interface CounterIntent: MVIIntent {
sealed interface DisplayingCounterIntent: MVIIntent
sealed interface ErrorIntent : MVIIntent
sealed interface LoadingIntent : MVIIntent
}
// then, use
IntentReceiver.DisplayingCounterContent()
```
## Step 5: Create Previews or UI Tests
When you have defined your `*Content` function, you will get a composable that can be easily used in previews.
That composable will not need DI, Local Providers from compose, or anything else for that matter, to draw itself.
But there's a catch: It has an `IntentReceiver` as a parameter. To deal with this, there is an `EmptyReceiver`
composable. EmptyReceiver does nothing when an intent is sent, which is exactly what we want for previews and UI tests.
We can now define our `PreviewParameterProvider` and the Preview composable.
You won't need the `EmptyReceiver` if you pass the `intent` callback manually.
```kotlin
private class StateProvider : CollectionPreviewParameterProvider(
listOf(DisplayingCounter(counter = 1), Loading)
)
@Composable
@Preview
private fun CounterScreenPreview(
@PreviewParameter(StateProvider::class) state: CounterState,
) = EmptyReceiver {
CounterScreenContent(state)
}
```
---
## Dependency Injection
DI is mostly out of scope of the library, however, setting up your own injection is very easy and can be done
with a single file because Containers and Stores are just like any other dependency: they can be injected as factories
or scoped. The only problem is with injecting Stores in a way that survives config changes in Android and follows
navigation graph lifecycle, which is solved below.
To inject a store that survives configuration changes, we can use (now multiplatform) `androidx.viewmodel` setup.
Jetpack navigation and DI frameworks already provide everything we need. If you are using a different navigation
library, your setup will be different, just make sure that the Stores are actually scoped to destinations and survive
configuration changes.
:::info
The examples below use the `Container` interface for convenience, but if you don't, your setup will be a bit different,
with an added manual call to `store.start()` upon creation of your ViewModel.
:::
## Koin 4.1.x
We only need 2 functions - one to declare, and another to inject the container:
```kotlin
@FlowMVIDSL
inline fun , S : MVIState, I : MVIIntent, A : MVIAction> Module.container(
crossinline definition: Definition,
) = viewModel(qualifier()) { params ->
ContainerViewModel(container = definition(params))
}
@FlowMVIDSL
@NonRestartableComposable
@Composable
inline fun , S : MVIState, I : MVIIntent, A : MVIAction> container(
key: String? = null,
scope: Scope = currentKoinScope(),
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current),
extras: CreationExtras = defaultExtras(viewModelStoreOwner),
noinline params: ParametersDefinition? = null,
): T = koinViewModel>(
qualifier = qualifier(),
parameters = params,
key = key,
scope = scope,
viewModelStoreOwner = viewModelStoreOwner,
extras = extras
).container
```
And our DI code is now 2 lines:
```kotlin
val accountModule = module {
container { new(::GoogleSignInContainer) }
container { new(::SignInContainer) }
}
@Composable
fun SignInScreen(
email: String,
container: SignInContainer = container { parametersOf(email) }, // parameters are passed to the container
) {
// or as a field
val googleSignIn: GoogleSignInContainer = container()
}
```
:::tip
Specify the return type to avoid defining 4 type parameters of the function manually.
:::
## [Kodein](https://github.com/kosi-libs/Kodein) 7.x
First of all, make sure you set up Kodein as in [docs](https://kosi-libs.org/kodein/7.25/framework/compose.html) and
provide DI using `withDI()`.
Then, with Kodein, the setup is also very simple, albeit a little bit scary-looking:
```kotlin
inline fun , S : MVIState, I : MVIIntent, A : MVIAction> DI.Builder.container(
@BuilderInference crossinline definition: NoArgBindingDI.() -> T
) = bind>() with provider { ContainerViewModel(definition()) }
@Suppress("INVISIBLE_REFERENCE", "INDENTATION")
@kotlin.internal.LowPriorityInOverloadResolution
inline fun <
reified T : Container,
reified P : Any,
S : MVIState,
I : MVIIntent,
A : MVIAction
> DI.Builder.container(
@BuilderInference crossinline definition: BindingDI.(P) -> T
) = bind>() with factory { params: P -> ContainerViewModel(definition(params)) }
@Composable
@NonRestartableComposable
inline fun , S : MVIState, I : MVIIntent, A : MVIAction> container(): T {
val vm by rememberViewModel>()
return vm.container
}
@Suppress("INVISIBLE_REFERENCE", "INDENTATION") // put in a separate package to remove the need for this suppress
@kotlin.internal.LowPriorityInOverloadResolution
@NonRestartableComposable
@Composable
inline fun , reified P : Any, S : MVIState, I : MVIIntent, A : MVIAction> container(
param: P,
): T {
val vm by rememberViewModel>(arg = param)
return vm.container
}
```
Then we can inject things with 2 lines of code:
```kotlin
val accountModule by DI.Module {
container { new(::GoogleSignInContainer) }
container { email: String -> new(email, ::SignInContainer) } // added in Kodein 7.26
}
@Composable
fun SignInScreen(
email: String,
container: SignInContainer = container(email),
) {
val googleSignIn: GoogleSignInContainer = container()
}
```
:::info
We need `@kotlin.internal.LowPriorityInOverloadResolution` to resolve the ambiguity between two of our functions.
If we add an annotation, we can specify parameter as shown in the example with `{ param: Int -> }` instead of
specifying all of 5 type arguments, and if we don't, the function will resolve to the no-argument version instead
of showing an error. This is currently a lacking implementation of proper overload resolution in Kotlin.
Also in Kodein 7.26+ a `new()` function with parameter support will be added instead of having to use `instance()`.
:::
## Hilt / Kotlin-inject
Unfortunately the authors are not currently using those libraries, so no examples can be provided.
If you are setting up those, you can join the chat on Slack to receive support.
If you already have a working setup, you can help other people by opening an issue and providing your code to add to
this page.
---
## Essenty Integration
The library integrates with Essenty (Decompose) to support lifecycle and retaining store instances across configuration
changes. The integration supports all the artifacts that Essenty supports.
```toml
# Includes retained stores and coroutine scopes
flowmvi-essenty = { module = "pro.respawn.flowmvi:essenty", version.ref = "flowmvi" }
# Includes lifecycle support for store subscription
flowmvi-essenty-compose = { module = "pro.respawn.flowmvi:essenty-compose", version.ref = "flowmvi" }
```
## Retaining Stores
Creating a store that
is [retained](https://arkivanov.github.io/Decompose/component/instance-retaining/#instance-retaining) across
configuration changes is ideally done as follows:
```kotlin
// inject dependencies and write your logic as usual
class FeatureContainer(
private val repo: CounterRepository,
) {
val store = store(Loading) {
// ...
}
}
class CounterComponent(
context: ComponentContext,
container: () -> FeatureContainer = { inject() }, // inject via DI as a factory or provide manually
) : ComponentContext by context,
Store by context.retainedStore(factory = container) {
init {
subscribe {
actions.collect { action: CounterAction ->
}
}
}
}
```
The store that has been created will be started in a retained coroutine scope upon creation.
If you are using the `Container` interface, you can delegate that one as well.
You can override the scope by passing your own scope to the function:
```kotlin
retainedStore(
initial = Loading,
scope = retainedScope(),
key = "Type of the State class by default",
factory = { /* inject here */ },
)
```
Pass `null` to the scope to not start the store upon creation. In this case, you'll have to start the store yourself.
:::warning Caveat:
If you do not use the factory DSL and instead build a store that is retained, it will capture everything you
pass into the `builder` closure. This means that any parameters or outside properties you use in the builder will be
captured **and retained** as well. This is the same caveat that you have to be aware of when
using [Retained Components](https://arkivanov.github.io/Decompose/component/instance-retaining/#retained-components-since-v210-alpha-03).
If you don't want to retain your stores to prevent this from happening, just build the store
normally using a `store` builder. However, the store will be recreated and relaunched on configuration changes.
If you are using retained components already, you can opt-in to the warning annotation issues by the library using a
compiler flag or just not use retained stores.
:::
## Retaining Coroutine Scopes
By default, a store is launched using a `retainedScope`. As the name says, it's retained across configuration changes
and will be stopped when the `InstanceKeeper`'s storage is cleared. If you want to relaunch the store on lifecycle
events instead, pass a regular `coroutineScope` from essenty coroutine extensions. All stores will share the same scope
by default.
```kotlin
class CounterComponent(
componentContext: ComponentContext,
) : ComponentContext by componentContext {
private val scope = coroutineScope()
val store = store(Loading, scope) { /* ... */ }
}
```
## Subscribing in Components
You can subscribe in the `init` of your component as shown above. The subscription will follow the component's
lifecycle. It's preferable to collect `MVIAction`s in one place only (either UI or the component) because otherwise you
will have to use `ActionShareBehavior.Share()` and manage multiple subscribers manually.
## Subscribing in the UI
The base `compose` artifact already provides you with everything that is necessary to implement lifecycle support.
The `essenty-compose` artifact simplifies the provision of lifecycle to the UI that subscribes to the stores.
When you create a component, it is assigned a new `Lifecycle` instance by Decompose.
This lifecycle can be used on the UI to correctly subscribe to the store.
```kotlin
@Composable
fun CounterScreen(component: CounterComponent) {
// when overriding Container
val state by component.subscribe()
// when just using a property
val state by component.store.subscribe(component)
}
```
Optionally, you can override the `Lifecycle.State` parameter to specify when the store should unsubscribe.
By default, the store will unsubscribe when:
* The component goes into the backstack
* The composable goes out of the composition
* The lifecycle reaches the `STOPPED` state, such as when the UI is no longer visible, but is still composed.
:::tip
The requirements for setting up lifecycle correctly are the same as in
the [Decompose docs](https://arkivanov.github.io/Decompose/component/lifecycle/).
:::
If you want another approach, you can provide the lifecycle via a `CompositionLocal`:
```kotlin
@Composable
fun CounterScreen(component: CounterComponent) {
// do this somewhere in your navigation logic
ProvideSubscriberLifecycle(component) {
val state by component.store.subscribe(DefaultLifecycle)
}
}
```
The `DefaultLifecycle` property will try to first find the lifecycle you provided. If not found, it will try to
resolve the system lifecycle (which should always be present since compose 1.6.10).
---
## Testing with the FlowMVI Test DSL
FlowMVI ships a small, coroutine-friendly test harness in the `pro.respawn.flowmvi:test` module.
Itβs intended for unit-testing Stores and StorePlugins with safer concurrency and easier lifecycle management.
## Setup
Add the test module:
```toml
[versions]
flowmvi = ""
[libraries]
flowmvi-test = { module = "pro.respawn.flowmvi:test", version.ref = "flowmvi" }
```
```kotlin
dependencies {
val flowmvi = ""
commonTestImplementation("pro.respawn.flowmvi:test:$flowmvi")
}
```
For flow assertions, FlowMVIβs own tests use Turbine (`app.cash.turbine:turbine`), but any Flow test utility works.
## Keep test configuration separate (recommended)
Itβs a good practice to keep a dedicated Store configuration for tests and provide it via DI, instead of inlining test-only
flags in every Store builder.
This keeps tests deterministic and makes it easy to switch concurrency knobs (e.g. `stateStrategy`, overflow behavior, logging).
The sample app demonstrates this pattern with a DI-provided `ConfigurationFactory` and a test variant:
- `sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/ConfigurationFactory.kt`
- `sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/TestConfigurationFactory.kt`
Typical usage inside a Store/Container builder:
```kotlin
class MyContainer(
private val configuration: ConfigurationFactory,
) {
val store = store(InitialState) {
configuration(name = "MyStore")
// plugins/reduce/etc
}
}
```
To learn how to provide/override that dependency in your DI setup, see [Dependency Injection](/integrations/di).
## Testing Stores
There are two entry points:
- `Store.test { ... }` starts the store, runs your block, then closes the store (no subscription).
- `Store.subscribeAndTest { ... }` starts the store, subscribes, runs your block **inside the subscription scope**, then
unsubscribes and closes the store.
### `test { }`: lifecycle-focused
Use `test { }` when you want to assert startup/shutdown behavior, subscription counts, or βstore is still aliveβ
invariants.
The receiver is a `TestStore` (a `Store` plus `SubscriptionAware`).
```kotlin
store.test {
isActive // StoreLifecycle
subscriberCount.value // SubscriptionAware
// send intents
emit(MyIntent)
}
```
### `subscribeAndTest { }`: state/actions-focused
Use `subscribeAndTest { }` when you want to assert emitted state transitions and actions.
The receiver is `StoreTestScope`, which delegates both the `Store` and the subscription `Provider`, so you get:
- `states` (a `StateFlow`) and `actions` (a `Flow`) from the subscription
- `emit(...)` / `intent(...)` to send intents
Typical pattern with Turbine:
```kotlin
store.subscribeAndTest {
states.test {
awaitItem() shouldBe InitialState
intent(MyIntent)
awaitItem() shouldBe ExpectedState
}
actions.test {
intent(MyIntentThatSendsAction)
awaitItem() shouldBe ExpectedAction
}
}
```
:::caution[Transient subscription validation]
`subscribeAndTest { ... }` intentionally **returns** from the subscription block (it unsubscribes when your test block
ends).
If your store runs with `debuggable = true`, make sure your store configuration allows transient subscriptions
(`allowTransientSubscriptions = true`), otherwise the store may treat the finished subscription as a bug.
:::
## Testing Plugins
Use `pro.respawn.flowmvi.test.plugin.test` on a `LazyPlugin`:
```kotlin
myPlugin.test(
initial = InitialState,
configuration = {
debuggable = true
// coroutineContext = ...
// verifyPlugins = ...
// name = ...
},
) {
// PluginTestScope
}
```
### What the plugin harness provides
Inside `test { ... }`, you get a `PluginTestScope` that is:
- a `PipelineContext` (so you can call `emit`, `intent`, `updateState`, `withState`, `send`/`action`, etc.)
- a `StorePlugin` (so you can directly call plugin callbacks like `onStart`, `onIntent`, `onState`, `onException`, β¦)
- a `ShutdownContext`/`StoreLifecycle` (so the scope can be cancelled via `closeAndWait()`; this happens automatically
after the block)
The harness also installs a `TimeTravel` plugin (exposed as `timeTravel`) to observe store events the plugin causes.
### Lifecycle-driven plugins
The harness does **not** implicitly call lifecycle callbacks for you. If your plugin reacts to lifecycle, call them
explicitly:
```kotlin
plugin.test(initial = InitialState) {
onStart()
onSubscribe(1)
// ...
onUnsubscribe(0)
onStop(null)
}
```
### Asserting via `timeTravel`
`timeTravel` is usually the simplest way to assert what happens inside your plugin from the outside of it:
```kotlin
plugin.test(initial = InitialState) {
onStart()
onIntent(MyIntent)
timeTravel.actions.last() shouldBe ExpectedAction // plugin emitted a side-effect
}
```
### Configuration options
The `configuration` lambda is a `StoreConfigurationBuilder` and is applied to the mock pipeline context used in the
test.
Common toggles for tests:
- `debuggable`: enable extra checks and debug logging
- `coroutineContext`: provide dispatcher overrides if the plugin launches coroutines
- `verifyPlugins`: force plugin verification on/off
- `allowIdleSubscriptions` / `allowTransientSubscriptions`: relevant if your plugin depends on subscription rules
## Real examples
FlowMVIβs own test suite is a good set of reference patterns:
- Store tests using `subscribeAndTest`: `core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreEventsTest.kt`
- Store lifecycle tests using `test`: `core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt`
- Plugin tests using `LazyPlugin.test`: `core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/plugin/ReducePluginTest.kt`
- Subscription-driven plugin tests:
`core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/plugin/WhileSubscribedPluginTest.kt`
---
## Contributing
* To build the project, you will need the following in your local.properties:
```properties
# only required for publishing (maintainers)
sonatypeUsername=...
sonatypePassword=...
signing.key=...
signing.password=...
keystore.password=...
# always required
sdk.dir=...
release=false
```
* Make sure you have these installed:
* Android Studio latest Stable or Beta, depending on the current project's AGP.
* Kotlin Multiplatform suite (run `kdoctor` to verify proper setup)
* Detekt plugin
* Kotest plugin
* Compose plugin
* JDK 22+ (for both building and running gradle)
* To debug the project, use the tasks provided as IDEA run configurations
* All tests to run tests
* All benchmark to benchmark the library
* Sample \ to test the sample app manually
* Publish to Local to upload a maven local build to test in your project
* Before pushing, make sure the following tasks pass:
* `gradle detektFormat`
* `gradle assemble`
* `gradle allTests`
* If you submit a PR that changes behavior, please add tests for the changes.
* All contributions are welcome, including your plugin ideas or plugins you used in your project.
* We're especially looking for people who use FlowMVI in an iOS-compatible KMP project because we would like to include
the adapters and solutions people came up with
to the core library to improve overall experience of library users out-of-the-box.
---
## FAQ
### How to fix "Cannot inline bytecode" error?
The library's minimum JVM target is set to 11 (sadly still not the default in Gradle).
If you encounter an error:
```
Cannot inline bytecode built with JVM target 11 into bytecode that
is being built with JVM target 1.8. Please specify proper '-jvm-target' option
```
Then configure your kotlin multiplatform compilation to target JVM 11 in your subproject's `build.gradle.kts`:
```kotlin
kotlin {
androidTarget { // do the same for JVM/desktop target as well
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
}
```
And in your android gradle files, set:
```kotlin
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
```
If you support Android API \<26, you will also need to
enable [desugaring](https://developer.android.com/studio/write/java8-support).
### How to name Intents, States, Actions?
There is an ongoing discussion on how to name Intents/States/Actions.
Here's an example of rules we use at [Respawn](https://respawn.pro) to name our Contract classes:
* `MVIIntent` naming should be ``.
Example: `ClickedCounter`, `SwipedDismiss` (~~CounterClick~~).
* `MVIAction`s should be named using verbs in present tense. Example: `ShowConfirmationPopup`, `GoBack`.
* Navigation actions should be using the `GoTo` verb (~~NavigateTo, Open...~~) Example: `GoToHome`.
Do not include `Screen` postfix. `GoToHome`~~Screen~~.
* `MVIState`s should be named using verbs in present tense using a gerund. Examples: `EditingGame`, `DisplayingSignIn`.
### My intents are not reduced! When I click buttons, nothing happens, the app just hangs.
* Did you call `Store.start(scope: CoroutineScope)`?
* Did you call `Store.subscribe()`?
### My Actions are not consumed, are dropped or missed.
1. Examine if you have `subscribe`d to the store correctly. Define the lifecycle state as needed.
2. Check your `ActionShareBehavior`. When using `Share`, if you have multiple subscribers and one of them is not
subscribed yet, or if the View did not manage to subscribe on time, you will miss some Actions.
This is a limitation of the Flow API and there are no potential
resolutions at the moment of writing. Try to use `Distribute` instead.
3. If one of the subscribers doesn't need to handle Actions, you can use another overload of `subscribe` that does not
subscribe to actions.
4. Try to use an `onUndeliveredIntent` handler of a plugin or install a logging plugin to debug missed events.
### In what order are intents, plugins and actions processed?
* Intents: FIFO or undefined based on the configuration parameter `parallelIntents`
* Actions: FIFO
* States: FIFO
* Plugins: FIFO (Chain of Responsibility) based on installation order
* Decorators: FIFO, but after all of the regular plugins
### When I consume an Action, the other actions are delayed or do not come
Since actions are processed sequentially, make sure you launch a coroutine to not prevent other actions from coming and
suspending the scope. This is particularly obvious with things like snackbars that suspend in compose.
### I want to expose a few public functions in my container for the store. Should I do that?
You shouldn't. Use an Intent / Action to follow the contract, unless you are using `LambdaIntent`s.
In that case, expose the parent `ImmutableContainer` / `ImmutableStore` type to hide the `intent` function from
subscribers.
### How to use androidx.paging?
Well, this is a tricky one. `androidx.paging` breaks the architecture by invading all layers of your app with UI
logic. The best solution we could come up with is just passing a PagingFlow as a property in the state.
This is not good, because the state becomes mutable and non-stable, but there's nothing better we could come up with,
but it does its job, as long as you are careful not to recreate the flow and pass it around between states.
The Paging library also relies on the `cachedIn` operator which is tricky to use in `whileSubscribed`, because that
block is rerun on every subscription, recreating and re-caching the flow.
To fix this issue, use `cachePlugin` to cache the paginated flow, and then pass it to `whileSubscribed` block.
This will prevent any leaks that you would otherwise get if you created a new flow each time a subscriber appears.
```kotlin
val pagingFlow by cache {
repo.getPagingDataFlow().cachedIn(this)
}
```
### I have a lot of data streams. Do I subscribe to all of the flows in my store?
It's preferable to create a single flow using `combine(vararg flows...)` and produce your state based on that.
This will ensure that your state is consistent and that there are no unnecessary races in your logic.
As flows add up, it will become harder and harder to keep track of things if you use `updateState` and `collect`.
### But that other library has 9000 handlers, reducers and whatnot. Why not do the same?
In general, a little boilerplate when duplicating intents is worth it to keep the consistency of actions and intents
of screens intact.
You usually don't want to reuse your actions and intents because they are specific to a given screen or flow.
That makes your logic simpler, and the rest can be easily moved to your repository layer, use cases or just plain
top-level functions. This is where this library is opinionated, and where one of its main advantages - simplicity, comes
from. Everything you want to achieve with inheritance can already be achieved using plugins or child/parent
stores. For example, if you still want a reducer object and a plugin for it, all you have to do is:
```kotlin
fun interface Reducer {
operator fun S.invoke(intent: I): S
}
fun StoreBuilder.reduce(
reducer: Reducer
) = reducePlugin(consume = true) {
updateState {
with(reducer) { invoke(it) }
}
}.install()
```
### How to avoid class explosion?
1. Modularize the app. The library allows to do that easily.
2. Use nested classes. For example, define an `object ScreenContract` and nest your state, intents, and actions
inside to make autocompletion easier.
3. Use `LambdaIntent`s. They don't require subclassing `MVIIntent`.
4. Disallow Actions for your store. Side effects are sometimes considered an anti-pattern, and you may want to disable
them if you care about the architecture this much.
### I want to use a resource or a framework dependency in my store. How can I do that?
The best solution would be to avoid using platform dependencies such as string resources.
Instead, you could delegate to the UI layer - the one entity that **should** be handling data representation.
That would result in creating a few more intents and other classes, but it will be worth it to achieve better SoC.
If you are not convinced, you could try to use a resource wrapper. The implementation will vary depending on your needs.
---
## More Resources
On this page you can find additional resources and projects to be inspired by or get practical examples
## Sample apps
### [Meeting Cost](https://github.com/respawn-app/meetingcost)
This is a simple one-page application to calculate meeting costs in real-time.
It features the simplest setup to get started with the library and implement a functional app.
- Stack: Compose Multiplatform, FlowMVI, no DI.
- Platforms: Wasm, Desktop
- Difficulty: Beginner
### [Official Sample App](https://github.com/respawn-app/FlowMVI/tree/master/sample)
The official sample app outlines navigation setup with Decompose, multiple platforms, showcases native Android
integration, DI setup & more. It's built in such a way as to be deployed to
the [Web](https://opensource.respawn.pro/FlowMVI/sample).
Run the app to see a showcase of main features, or explore code to see an extensive app with navigation & more.
- Stack: CMP, FlowMVI, Koin, Decompose, ApiResult.
- Platforms: Android, Wasm, Desktop
- Difficulty: Advanced
## Articles
### [How FlowMVI Changed the Fate of Our Project](https://medium.com/proandroiddev/success-story-how-flowmvi-has-changed-the-fate-of-our-project-3c1226890d67)
The article is a case study of how we saved a startup dying from complexity, crashes,
bugs and technical debt and implemented many new features along the way with FlowMVI.
### [How I Built a Game Engine using Kotlin](https://medium.com/proandroiddev/how-i-made-a-game-engine-using-mvi-in-kotlin-4472d758ad05)
This is a case study of how a developer implemented a fully-fledged Game Engine using FlowMVI, removed 7 000 lines of
code and replaced them with FlowMVI features, and managed complexity of the new customer requirements.
## "Used By"
### [respawn.pro](https://respawn.pro)
A project from the author of the library that inspired its creation. It's a productivity app with many features
and platform integrations that are fully powered by FlowMVI.
- Platforms: iOS, Android
### [overplay.com](https://overplay.com)
A startup that builds a gaming platform where anyone can turn their video into a game.
- Platforms: iOS, Android
## Your project
If you have used FlowMVI in an app, written an article, or created a video on it, just open an issue with a link to your
project and it will be added here!
---
## Creating custom plugins
Plugin is a unit that can extend the business logic of the Store.
All stores are mostly based on plugins, and their behavior is entirely determined by them.
* Plugins can influence subscription, stopping, intent handling, and all other forms of store behavior.
* Plugins are executed in the order they were installed and follow the Chain of Responsibility pattern.
* Access the store's context & configuration and launch jobs through the `PipelineContext` receiver.
* Plugins are highly optimized to conflate any operations not defined, which means you do not need to worry about having
too many plugins. The bottleneck will always be the longest chain of callbacks, not the plugin count. If you don't
define a callback, then no CPU time is spent on running it and no memory is allocated.
If you want to install a custom Plugin, use the methods of the Store builder:
```kotlin
override val store = store(Loading) {
// install an existing plugin
install(
analyticsPlugin(),
diScopePlugin(),
// ...
)
// or build on-the-fly
install {
onIntent { intent ->
analytics.logUserAction(intent.name)
}
}
}
```
## Creating an Eager Plugin
Plugins are simply built:
```kotlin
val plugin = plugin {
// dsl for intercepting is available
}
```
You can generate a new generic plugin using the `fmvip` [IDE Plugin](https://plugins.jetbrains.com/plugin/25766-flowmvi)
shortcut.
### Lazy Plugins
Lazy plugins are created **after** the store builder has been run (they are still installed in the order they were
declared). This gives you access to the `StoreConfiguration`, which contains various options, of which the most useful
are:
* `StoreLogger` instance,
* Store `name`,
* `debuggable` flag,
* `initial` state.
To create a lazy plugin, use `lazyPlugin` builder function. It contains a `config` property:
```kotlin
val resetStatePlugin = lazyPlugin {
if (!config.debuggable) config.logger(Warn) { "Plugin for store '${config.name}' is installed on a release build" }
onException {
updateState { config.initial } // reset the state
null
}
}
```
* You can generate a lazy plugin using the `fmvilp`
[IDE Plugin](https://plugins.jetbrains.com/plugin/25766-flowmvi) shortcut.
* You may not need to use a lazy plugin because `PipelineContext` has the `config` property too. If you miss `config` in
the builder body itself, then use a lazy plugin.
## Plugin DSL
Each property of a plugin has a different set of responsibilities and some usage nuances. Here's an explanation of all
of them:
### Name
```kotlin
val name: String? = null
```
The name can be used for logging purposes, but most importantly, to distinguish between different plugins.
Name is optional, when it is missing, the plugins will be compared **by reference**.
If you need to have the same plugin installed multiple times, consider giving plugins different names.
Plugins that have no name can be installed multiple times, assuming they are different instances of a plugin.
:::warning
If you attempt to install the same plugin multiple times, or different plugins
with the same name, **an exception will be thrown**.
:::
Consider the following examples:
``` kotlin
loggingPlugin("foo")
analyticsPlugin("foo") // -> will throw
loggingPlugin(null)
analyticsPlugin(null) // -> OK
loggingPlugin("plugin1")
loggingPlugin("plugin1") // -> will throw
loggingPlugin("plugin1")
loggingPlugin("plugin2") // -> OK, but same logs will be printed twice
loggingPlugin(null)
loggingPlugin(null) // -> OK, but same logs will be printed twice
val plugin = loggingPlugin(null)
install(plugin)
install(plugin) // -> will throw
```
So name your plugin based on whether you want it to be repeatable, i.e. installed multiple times.
For example, the library's `reduce` plugin **cannot** be installed multiple times by default.
### onState
```kotlin
suspend fun PipelineContext.onState(old: S, new: S): S? = new
```
A callback to be invoked each time `updateState` is called.
This callback is invoked **before** the state changes, but **after** the consumer function's `block` is invoked.
Any plugin can veto (forbid) or modify the state change.
This callback is **not** invoked at all when state is changed through `updateStateImmediate`, used in `withState`
or when `state` is obtained directly.
* Return `null` to cancel the state change. All plugins registered later when building the store will not receive
this event.
* Return `new` to continue the chain of modification, or allow the state to change,
if no other plugins change it.
* Return `old` to veto the state change, but allow next plugins in the queue to process the state.
* Execute other operations using `PipelineContext`, including jobs.
* If the store has `atomicStateUpdates`, then this block will already be invoked in the transaction context.
* Avoid calling `updateState` here, as it will result in an infinite loop!
### onIntent
```kotlin
suspend fun PipelineContext.onIntent(intent: I): I? = intent
```
A callback that is invoked each time an intent is received **and then begun** to be processed.
This callback is invoked **after** the intent is sent and **before** it is received,
sometimes the time difference can be significant if the store was stopped for some time
or even **never** if the store's buffer overflows or store is not ever used again.
* Return `null` to veto the processing and prevent other plugins from using the intent.
* Return another intent to replace `intent` with another one and continue with the chain.
* Return `intent` to continue processing, leaving it unmodified.
* Execute other operations using `PipelineContext`.
* Generally, you can send other intents inside this handler, but watch out for infinite loops.
### onIntentEnqueue
```kotlin
fun onIntentEnqueue(intent: I): I? = intent
```
Invoked immediately before an intent is enqueued into the store buffer (pre-buffer).
Return `null` to drop, or a transformed intent to enqueue instead.
:::warning
This callback runs outside the store pipeline. Exceptions thrown here will **not** be caught by `onException`/recover
and will be thrown to the caller of `intent/emit`. Keep it fast and exception-safe.
:::
### onAction
```kotlin
suspend fun PipelineContext.onAction(action: A): A? = action
```
A callback that is invoked each time an `MVIAction` has been sent.
This is invoked **after** the action has been sent by store's code, but **before** the subscriber handles it.
This function will always be invoked, even after the action is later dropped because of `ActionShareBehavior`,
and it will be invoked before the `action(action: A)` returns, if it has been suspended, so this handler may suspend the
parent coroutine that wanted to send the action.
* Return `null` to veto the processing and prevent other plugins from using the action.
* Return another action to replace `action` with another one and continue with the chain.
* Return `action` to continue processing, leaving it unmodified.
* Execute other operations using `PipelineContext`
* Generally, you can send other Actions here.
### onActionDispatch
```kotlin
fun onActionDispatch(action: A): A? = action
```
Invoked after an action is dequeued and before it is delivered to subscribers.
Return `null` to drop, or transform the action before delivery.
:::note
Invocation semantics depend on the store's `config.actionShareBehavior`:
- `Distribute` / `Restrict` (channel-backed): invoked once per action delivery (single subscriber consumes it).
- `Share` (shared-flow-backed): invoked once per subscriber collecting `actions` (and also for replayed actions).
:::
:::warning
This callback also executes outside the recoverable pipeline. Exceptions thrown here will **not** reach `onException`
and will escape to the caller that triggered delivery. Make it non-throwing.
:::
### onException
```kotlin
suspend fun PipelineContext.onException(e: Exception): Exception? = e
```
A callback that is invoked when Store catches an exception. It is invoked when either a coroutine launched inside the
store throws, or when an exception occurs in any other plugin.
* If none of the plugins handles the exception (returns `null`), **the exception is rethrown and the store fails**.
* If you throw an exception in this block, **the entire thread will crash**. Do not throw exceptions in this function.
* This is invoked **before** the exception is rethrown or otherwise processed.
* This is invoked **asynchronously in a background job** and after the job that has thrown was cancelled, meaning
that some time may pass after the job is cancelled and the exception is handled.
* Handled exceptions do not result in the store being closed.
* You cannot prevent the job that threw an exception and all its nested jobs from failing. The job has already been
canceled and can no longer continue. This does not apply to the store's context however.
-----
* Return `null` to signal that the exception has been handled and recovered from, continuing the flow's processing.
* Return `e` if the exception was **not** handled and should be passed to other plugins or rethrown.
* Execute other operations using `PipelineContext`
### onStart
```kotlin
suspend fun PipelineContext.onStart(): Unit = Unit
```
A callback that is invoked **each time** `Store.start` is called.
* Suspending in this callback will **prevent** the store from starting until the plugin is finished.
* Plugins that use `onSubscribe` will also not get their events until this is run and no intents will be processed.
* Execute any operations using `PipelineContext`.
### onSubscribe
```kotlin
suspend fun PipelineContext.onSubscribe(subscriberCount: Int): Unit = Unit
```
A callback to be executed **each time** `Store.subscribe` is called.
* This callback is executed **after** the `subscriberCount` is incremented i.e. with the **new** count of subscribers.
* There is no guarantee that the subscribers will not be able to subscribe when the store has not been started yet.
But this function will be invoked as soon as the store is started, with the most recent subscriber count.
* This function is invoked in the store's scope, not the subscriber's scope.
* There is no guarantee that this will be invoked exactly before a subscriber reappears.
It may be so that a second subscriber, for example,
appears before the first one disappears (due to the parallel nature of
coroutines). In that case, `onSubscribe` will be invoked first as if it was a second subscriber, and then
`onUnsubscribe` will be invoked, as if there were more subscribers for a moment.
* Suspending in this function will prevent other plugins from receiving the subscription event (i.e. next plugins
that use `onSubscribe` will wait for this one to complete.
### onUnsubscribe
```kotlin
suspend fun PipelineContext.onUnsubscribe(subscriberCount: Int): Unit = Unit
```
A callback to be executed when the subscriber cancels its subscription job (unsubscribes).
* This callback is executed **after** the subscriber has been removed and **after** `subscriberCount` is
decremented. This means, for the last subscriber, the count will be 0.
* There is no guarantee that this will be invoked exactly before a subscriber reappears.
It may be so that a second subscriber appears before the first one disappears (due to the parallel nature of
coroutines). In that case, `onSubscribe` will be invoked first as if it was a second subscriber, and then
`onUnsubscribe` will be invoked, as if there were more subscribers for a moment.
* Suspending in this function will prevent other plugins from receiving the unsubscription event (i.e. next plugins
that use `onUnsubscribe` will wait for this one to complete.
### onStop
```kotlin
fun ShutdownContext.onStop(e: Exception?): Unit = Unit
```
Invoked when the store is closed.
* This is called **after** the store is already closed, and you cannot influence the outcome.
* This is invoked for both exceptional stops and normal stops.
* Will not be invoked when an `Error` is thrown. You should not handle `Error`s.
* `e` is the exception the store is closed with. Will be `null` for normal completions.
* You can update the state in the `ShutdownContext`, but generally avoid relying on thread safety here, as this callback
is invoked synchronously on a **random thread** and in a **random context**
* This function should always be fast and non-blocking, and **not** throw exceptions, or the entire coroutine
machinery will fall apart.
### onUndeliveredIntent
```kotlin
fun ShutdownContext.onUndeliveredIntent(intent: I): Unit = Unit
```
Called when an intent is not delivered to the store.
This can happen, according to the `Channel`'s documentation:
* When the store has a limited buffer and it overflows.
* When store is stopped before this event could be handled, or while it is being handled.
* When the `onIntent` function throws an exception that is not handled by the `onException` block.
* When the store is stopped and there were intents in the buffer, in which case, `onUndeliveredIntent` will
be called on all of them.
:::warning
This function is called in an undefined coroutine context on a random thread,
while the store is running or already stopped. It should be fast, non-blocking,
and must **not throw exceptions**, or the entire coroutine machinery will fall apart.
The `onException` block will **not** handle exceptions in this function.
:::
### onUndeliveredAction
```kotlin
fun ShutdownContext.onUndeliveredAction(action: A): Unit = Unit
```
Called when an action is not delivered to the store.
This can happen:
* When the Store's `ActionShareBehavior` is `ActionShareBehavior.Distribute` or `ActionShareBehavior.Restrict`.
In this case, depending on the configuration, the queue of actions may have a limited buffer and overflow.
* When store is stopped before this event could be received by subscribers.
* When the subscriber cancels their subscription or throws before it could process the action.
* When the store is stopped and there were actions in the buffer, in which case, `onUndeliveredAction` will
be called on all of them.
:::warning
This function is called in an undefined coroutine context on a random thread,
while the store is running or already stopped. It should be fast, non-blocking,
and must **not throw exceptions**, or the entire coroutine machinery will fall apart.
The `onException` block will **not** handle exceptions in this function.
:::
---
## Remote Debugging
FlowMVI comes with a remote debugging setup with a dedicated Jetbrains IDEs / Android Studio plugin and a desktop app
for Windows, Linux, and MacOS.
## Step 1: Install the plugin on **debug builds only**
:::danger[Don't install the debugger on prod builds!]
It pollutes your app with unnecessary code, introduces serious security
risks and degrades performance. If possible on your platform, don't include the debugging code in the release build or
use minification/obfuscation to remove the debugging code.
:::
### 1.1 Set up a module for store configurations
To keep the source set structure simple, you can create a separate module for your store configuration logic and then
inject configurations using DI.
First, create a separate module where you'll keep the Store configuration.
```
/
ββ common-arch/
β ββ src/
β β ββ androidDebug/
β β β ββ InstallDebugger.kt
β β ββ commonMain/
β β β ββ InstallDebugger.kt
β β ββ nativeMain/
β β β ββ InstallDebugger.kt
β β ββ androidRelease/
β β β ββ InstallDebugger.kt
| ββ build.gradle.kts
```
```kotlin title="common-arch/build.gradle.kts"
dependencies {
debugImplementation(libs.flowmvi.debugger) // android Debug (name is incorrect on the kotlin plugin side)
nativeMainImplementation(libs.flowmvi.debugger) // other platforms
implementation(libs.flowmvi.core)
}
```
### 1.2 Set up source set overrides
Now we're going to create an expect-actual fun to:
1. Install the real remote debugger in `androidDebug` source set
2. Do nothing in `androidRelease` source set
3. Conditionally install the debugger on other platforms where build types are not supported.
```kotlin
// commonMain -> InstallDebugger.kt
expect fun StoreBuilder.remoteDebugger()
// androidDebug -> InstallDebugger.kt
actual fun StoreBuilder.remoteDebugger(
) = install(debuggerPlugin())
// androidRelease -> InstallDebugger.kt
actual fun StoreBuilder.remoteDebugger() = Unit
// conditional installation for other platforms:
actual fun StoreBuilder.remoteDebugger() {
enableRemoteDebugging()
}
```
:::info[About Indexing]
As of the date of writing, the Android Studio will not index the `androidRelease` source set correctly, but it _will_
be picked up by the compiler. We'll have to resort to "notepad-style coding" for that set unfortunately.
:::
### 1.3 Set up store configuration injection
:::tip
If you're building a small pet project, you may omit this complicated setup and just use a simple extension
if you know the risks you are taking.
:::
Set up config injection using a factory pattern using your DI framework:
```kotlin
interface ConfigurationFactory {
operator fun StoreBuilder.invoke(name: String)
}
inline fun StoreBuilder.configure(
configuration: StoreConfiguration,
name: String,
) = with(configuration) {
invoke(name = name)
}
```
:::tip
You can also use this to inject other plugins, such as the Saved State plugin or your custom plugins.
:::
Now we'll create a configuration factory.
You can create more based on your needs, such as for testing stores or app flavors.
```kotlin
internal class DefaultConfigurationFactory(
analytics: Analytics,
) : ConfigurationFactory {
override operator fun StoreBuilder.invoke(
name: String,
) {
configure {
this.name = name
debuggable = BuildFlags.debuggable // set up using an expect-actual
actionShareBehavior = ActionShareBehavior.Distribute()
onOverflow = SUSPEND
parallelIntents = true
logger = CustomLogger
}
enableLogging()
enableRemoteDebugging()
install(analyticsPlugin(analytics)) // custom plugins
}
}
```
Finally, inject your config:
```kotlin
internal class CounterContainer(
configuration: ConfigurationFactory,
) : Container {
override val store = store(Loading) {
configure(configuration, "Counter")
}
}
```
Setting up injection is covered in the [DI Guide](/integrations/di.md)
## Step 2: Connect the client on Android
:::info
You can skip this step if you don't target Android
:::
On all platforms except Android, we can just use the default host and port for debugging (localhost). But if you
use an external device or an emulator on Android, you need to configure the host yourself.
For emulators, the plugin will use the emulator host by default (`10.0.2.2`). We will need to allow cleartext traffic on
that host and our local network hosts
In your `common-arch` module we created earlier, or in the `app` module, create a network security configuration
**for debug builds only**.
```xml title="app/src/debug/AndroidManifest.xml"
```
```xml title="app/src/debug/res/xml/network_security_config.xml"
10.0.2.2192.168.*
```
:::warning
Please don't do this for release builds.
:::
## Step 3.1: Install and run the debugger app for a single device
Either install the IDE plugin by clicking the card on top, or install the desktop app from the Artifacts section of the
repository.
You can find the latest archive on the [releases](https://github.com/respawn-app/FlowMVI/releases) page on GitHub.
Run the debugger. The panel will ask you to configure host and port. Unless you are using a physical external device,
you can just use the defaults. Your devices must be on the same network to connect successfully.

Run the server and the client, or click the panel icon in the IDE.
After a few seconds, your devices should connect and you can start debugging.
## Step 3.2 External device configuration
If you are connected to an external device via ADB that is on the same network, you can set up the debugger to work with
that.
The setup is a little bit more complicated, but in short, it involves:
1. Assign a static IP address to both your PC and development device on your Wi-Fi network for convenience.
2. Use the IP address of your PC as the host address when running the debugger plugin.
3. Provide the IP address of the PC to the debugger store plugin (in the code) to let it know to which address to connect to using the
plugin parameters.
4. Make sure the debugging port you are using is open on both devices.
## Step 4: Visualizing Metrics (Optional)
The debugger can also display [metrics](/plugins/metrics.md) collected from your stores in real-time.
This gives you insight into store performance characteristics like intent throughput, state transition latency,
queue times, and subscription counts directly in the IDE plugin or desktop app.
### 4.1 Add the metrics dependency
```toml
flowmvi-metrics = { module = "pro.respawn.flowmvi:metrics", version.ref = "flowmvi" }
```
```kotlin
commonMainImplementation("pro.respawn.flowmvi:metrics:")
```
### 4.2 Configure metrics collection with DebuggerSink
Use `DebuggerSink` to send metrics to the debugger:
```kotlin
val store = store(Initial) {
val metrics = collectMetrics(reportingScope = applicationScope)
reportMetrics(
metrics = metrics,
sink = if (Build.debuggable) DebuggerSink() else BackendSink(),
)
}
```
Once configured, the debugger will display metrics for each connected store alongside the event timeline,
allowing you to correlate performance data with specific intents and state changes.
For more details on metrics collection and available sinks, see the [Metrics Plugin](/plugins/metrics.md) documentation.
---
## Decorators = Plugins for Plugins
:::warning[Experimental Feature]
Decorators are currently experimental because their DSL is limited by Kotlin features and they
are less safe/performant to use. There will be a breaking change with them within a few updates.
They are also not tested enough as of 3.2 releases.
:::
Decorators are very similar to plugins, so the way they work may seem confusing at first.
In short,
> A Decorator **wraps** another plugin and _manages_ it **manually**.
If plugins are executed automatically as a Chain of Responsibility, Decorators instead decide whether to call
plugin methods **themselves**. If you return `null` from a plugin callback, you can halt the chain, but you cannot
"wrap" it, watch over the entire chain of plugins, or skip the execution altogether. That's where you would use a
decorator.
Plugins can be used in two ways:
## Decorating Plugins
A straightforward way is to create a decorator and call `decorate` on a **single** plugin:
```kotlin
val plugin = plugin {
onIntent {
// does stuff
it
}
}
val decorator = decorator {
name = "FilterInvalidDecorator"
onIntent { chain, intent ->
if (intent is InvalidIntent) return@onIntent null
chain.run { onIntent(intent) } // returns the result of the chain
}
}
val decoratedPlugin = decorator decorates plugin
```
As you can see, there is an additional parameter `chain` in the decorator callback. It is the plugin that we
wrap. Decorators can be applied to any plugin, so you can't know which exact plugin you are wrapping except by querying
its `name`.
:::info
The `chain` parameter is temporary and will become a receiver with a future Kotlin release. Right now,
calling the chain is awkward because you have to always wrap it in a `run` block to bring the `PipelineContext` into
the scope.
:::
:::warning
If you don't call the corresponding plugin method, it will be skipped entirely! This can result in very dangerous
behaviors if, for example, plugins initialize some resources in `onStart` and expect to have them elsewhere.
Always auto-complete and use the `chain` parameter in some way.
:::
* The value you return from the decorator callback will behave in the opposite way to plugins. You should consider it
the "final" value, not the "next" or intermediate value that will go further down the chain.
A safe bet is to return from the decorator whatever the `chain` invocation returns. This means that you did
not want to make modifications to the result.
* If you don't define a particular decorator callback, it will be "transparent" (not skipped).
* You can decorate decorators because after you decorate a plugin, you get a plugin as a result.
## Decorating Stores
This section makes my brain explode each time, but I will try to explain it as best as I can.
The interesting thing about FlowMVI is that Store is just a decorator.
What it does is receive a plugin chain, wrapped in a Composite pattern after it is built, so that Store thinks it
always only has a single plugin installed. It then, like a decorator, decides when and whether to call its plugin
methods, and does things (sets states etc.) based on the value returned by the chain.
Previously I said that when you decorate a plugin, you get a plugin as a result. What this means is that
1. Store is a Plugin of _itself_
2. Store is a Decorator of _itself_
3. Store decorates Decorators
4. Decorators decorate Plugins
I know that makes 0 sense whatsoever. But practically, when you decorate a Store, you can think of it like this:
1. First all of the installed plugins are merged into a single plugin.
2. The first decorator you install decorates that chain.
3. Then each one of your decorators decorates the previous ones in the order of installation.
4. Then the Store decorates the last decorator in the chain and becomes a Plugin as a result.
So,
* Decorators are installed **after** all plugins, no matter where they are declared.
* Decorators are installed in order among themselves. The one declared lower wraps the upper one
* Whatever the last returned decorator returns from a callback becomes the final value passed to the Store.
## Pre-made Decorators
The power of decorators enables some awesome features. You can take them as an example by examining their source code.
### BatchIntentsDecorator
```kotlin
fun batchIntentsDecorator(
mode: BatchingMode,
queue: BatchQueue = BatchQueue(),
name: String? = "BatchIntentsDecorator",
onUnhandledIntent: suspend PipelineContext.(intent: I) -> Unit = {}
): PluginDecorator
```
This one intercepts the intents coming through it and puts them in a queue. Based on the `BatchingMode` it will either
accumulate a given `Amount` of intents in the queue before flushing them as soon as the queue overflows, or
intercept all intents and flush them every `Time` interval.
When a child plugin returns an intent instead of consuming it the decorator calls `onUnhandledIntent`. In amount mode
the callback fires for every unhandled item and the last one is also returned so the store can keep its default
undelivered flow. In time mode flushing happens inside a background job, so each unhandled intent is just reported via
the callback. The default callback does nothing, preserving the historical behaviour where unhandled intents were
dropped.
It is useful when you want to save some resources and want to do computations in bursts.
By default, it can only be installed once per Store.
Install with `batchIntents(mode)`.
### Debounce Intents Decorator
```kotlin
fun debounceIntentsDecorator(
timeout: Duration,
name: String? = "DebounceIntents",
): PluginDecorator
fun debounceIntentsDecorator(
name: String? = "DebounceIntents",
timeoutSelector: suspend PipelineContext.(I) -> Duration,
): PluginDecorator
```
Debounces incoming intents, mirroring the semantics of `kotlinx.coroutines.flow.debounce`:
* Only the latest intent emitted after a period of inactivity is forwarded downstream.
* Intents arriving within the debounce window cancel the pending delivery and schedule a new one.
* If the selected timeout is non-positive, the intent is forwarded immediately.
* The delivery runs in a background job; if a new intent arrives while the previous one is still being processed,
that processing is cancelled.
Install with `debounceIntents(timeout)` or `debounceIntents(name, timeoutSelector)`:
```kotlin
@OptIn(ExperimentalFlowMVIAPI::class)
val store = store(initial = State()) {
debounceIntents(300.milliseconds)
}
```
Dynamic timeouts are useful when only some intents should be debounced:
```kotlin
@OptIn(ExperimentalFlowMVIAPI::class)
val store = store(initial = State()) {
debounceIntents { intent ->
when (intent) {
is Intent.SearchQueryChanged -> 300.milliseconds
else -> Duration.ZERO
}
}
}
```
### Conflate Decorator
```kotlin
fun conflateIntentsDecorator(
name: String? = "ConflateIntents",
crossinline compare: ((it: I, other: I) -> Boolean) = MVIIntent::equals,
): PluginDecorator
```
* Out of the box, Intents and Actions, unlike States, are not conflated. That means that if you send the same Intent
twice, it will trigger two rounds of processing. If you don't want that, you can install the decorator using
`conflateIntents()` or `conflateActions()`.
* Using provided `compare` function, it will drop the **second** intent if the previous was the same as this (second)
one,
if the function returns `true`.
It can be useful if you have a Store (Plugin) where the same intents/actions can be spammed a lot and you don't
want them processed repeatedly.
### IntentTimeoutDecorator
```kotlin
fun intentTimeoutDecorator(
timeout: Duration,
name: String? = "IntentTimeout",
crossinline onTimeout: suspend PipelineContext.(I) -> I? = { throw StoreTimeoutException(timeout) },
): PluginDecorator
```
* This decorator will measure the time it takes to execute `onIntent` and (by default) throw an exception if processing
takes longer than the `timeout` value.
* When the `onTimeout` block is invoked, the execution has already been canceled, so you cannot continue it.
It works with both `parallelIntents` and regular ones, but does not measure the time it takes to run a job `launch`ed
inside any of the chain links.
It can be useful when you want to prevent the Store from being stuck processing an Intent for long because of some heavy
operation or a bug. In the block, you can resend the intent to retry, or report an error, for example.
### RetryDecorator
Speaking of retry:
```kotlin
fun retryIntentsDecorator(
strategy: RetryStrategy,
name: String? = null,
selector: (intent: I, e: Exception) -> Boolean = { _, _ -> true },
): StoreDecorator
```
* This one will use the `selector` first to decide whether it should retry execution of the `onIntent` callback, and if
the block returns `true`, it will retry processing it using the provided `strategy`.
* If the `strategy` includes a delay of any kind, the decorator will move the processing to a separate coroutine to not
prevent other intents from being processed.
* You can use the following strategies:
* `RetryStrategy.ExponentialDelay` - each delay will be multiplied by the exponent. By default 2, 4, 8...
* `RetryStrategy.FixedDelay` - each delay will be the same length up to a max `retries`.
* `RetryStrategy.Once` - just retry once immediately without running asynchronously.
* `RetryStrategy.Immediate` - retry immediately (while blocking other intents, if `parallelIntents` is not used),
for up to `retries` times.
* `RetryStrategy.Infinite` - retry indefinitely and immediately until store is closed or succeeded. Very dangerous.
---
## Composing Stores
Stores can be nested in each other, and delegate some of their responsibilities to other Stores.
Accomplishing that is simple and done in 2 steps:
1. Optionally, the **parent** store `A` should manage lifecycle of its **child** `B`.
It's as simple as launching child `B` in `PipelineContext` of the parent `A` (e.g. `init` plugin)
2. The **principal** store `A` is going to delegate some of its responsibilities to the **delegate** `B`.
The stores A and B switch roles based on the direction of the flow of control.
```mermaid
graph TD
A[Store A] -->|Delegates to| B(Store B)
B -->|Emits State| A
A -->|Delegates to| C[Store C]
C -->|Emits State| A
```
## Child Stores
Often, your child stores will have a lifecycle that is the same (or shorter) than the parent store.
FlowMVI provides a DSL to launch child stores:
```kotlin
class ParentContainer(
child2: Store // may have been started externally
) : Container {
val child1 = store(ChildState.Loading) { /*...*/ }
override val store = store(State.Loading) {
this hasChild child1
// or:
installChild(child2, force = false, blocking = true)
}
}
```
- The code above starts `child1` and `child2` using `PipelineContext` of parent `store`.
- It means that as soon as `store` starts, children also start (asynchronously), and when `store` stops, it stops
all its children.
- `hasChild` is a nice DSL for `installChild` plugin and its overloads.
- If you want to wait until all children start **before** the parent starts, set `blocking` to true.
- While usually not needed, can save you some synchronization of side-effects and plugins at the cost of
parent startup time.
- `force` is a parameter that defines whether you want to attempt starting the store always or only if it isn't
already started. `force` is equal to `debuggable` by default to not crash your release app builds.
- Example: If `force` is `true` and `child2` is already started when `store` starts, the plugin will throw.
## Delegating to Stores
Now let's look at the opposite - **principal** Store may want to shift some of its work onto the **delegate**.
This is very useful for composing the logic of handling state and splitting responsibilities between stores.
For example, to delegate State, the principal store will likely define its state as derived from other states:
```kotlin
sealed interface FeedState : MVIState { /* ... */ } // delegate
sealed interface AccountState : MVIState { /* ... */ }
data class HomeState(
val feed: FeedState,
val account: AccountState,
) : MVIState
```
Then in our principal container, we can create a special delegate object:
```kotlin
class ParentContainer() : Container {
private val feedStore = store(FeedState.Loading) { /*...*/ }
private val accountStore = store(AccountState.Loading) { /* ... */ }
override val store = store(State.Loading) {
// [1]
val feedState by delegate(feedStore) { feedAction ->
// handle Actions from the delegate
}
// [2].
this hasChild accountStore
val accountState by delegate(accountStore, start = false)
// use delegated states
whileSubscribed {
combine(feedState, accountState) { feed, account ->
updateState { HomeState(feed, account) }
}.consume()
}
}
}
```
You can see at \[1\] that our `feedState` is a state derived from delegating to `feedStore`.
The function `delegate()` assigns the target store as **a child** of the surrounding store, and hooks into the parent
lifecycle to update the state and handle actions when (by default) the principal store has subscriptions.
In essence, it subscribes to the **delegate** (`feedStore`) when the `store` has other subscribers (UI etc.)
and updates `feedState` flow with values from `feedStore.state`.
It's roughly equivalent to calling:
```kotlin
val feedState = MutableStateFlow(feedStore.state)
this hasChild feedStore
whileSubscribed {
feedStore.subscribe { state ->
feedState.value = state
}
}
```
:::warning[Important detail]
You can see above that the `feedState` is **not** a direct mirror of the state of the `feedStore` -
it's only a projection. It can have **stale values** because it is **not updated** until the
principal store **has subscribers**.
:::
The behavior above is customizable, but is the default to conserve resources and ensure
the logic in delegates matches their expectations about when subscriptions happen.
Based on `mode` parameter, you can choose between:
- `WhileSubscribed` - the default outlined above.
- `Immediate` - the state will be a direct projection of the delegate's state, and the delegate will be subscribed to
as soon as the principal store starts
Additionally, the delegate is automatically assigned a **child** status and started.
If you don't want that, pass `start = false` when installing the delegate ( like in \[2\] )
### Actions from Delegates
An important caveat to discuss is how side effects are handled. Yes, principal store can subscribe to actions of
delegates, but that isn't really necessary. You can still subscribe to delegate
stores as usual where you would normally do (UI etc).
If you want to add special logic from principal to an action in a child, you need to ensure `ActionShareBehavior` (ASB)
of children works properly with this new requirement. With `Distribute` ASB, the actions are **split** evenly between
subscribers, which principal store _is one of_. In this case, either use `Distribute`, but handle **all** actions
from children in principal (do not split the logic), or use `Share` and manually ensure no actions are sent when
subscribers are not present. More details on this limitation are in the ASB docs.
### Plugin vs Delegate
With two ways to split logic, how do you choose now whether you should make a store or a plugin?
Here's a table to help you decide:
| Consideration | Prefer Plugins | Prefer Child Stores |
|------------------------------------|--------------------------------------------------|---------------------------------------------------------------------|
| **State Complexity** | Small to medium, tightly coupled | High complexity or loosely coupled, can be split into parts |
| **Intent Processing** | Act on the same set of intents in different ways | Intents modify distinct pieces of state and send subsets of Actions |
| **Subscriber Timing & Data Needs** | Mostly same between all of subscribers | Subscribers need the store state at differing times |
| **Component Lifecycles** | Same lifecycle as the parent | Can sometimes differ |
| **Performance** | When performance is critical | When the overhead of async processing per child store is acceptable |
In more detail:
- Prefer plugins when you want to act on mostly **the same set** of intents in different ways.
If you want to split intent handling logic into pieces (with their own side effects and states), stores may be a
better fit.
- If performance is of critical importance, prefer plugins. They are lightweight, in contrast,
child stores introduce a layer of coroutines and async processing for **each** new child store.
- If your **state has grown** increasingly complex, with many intents mutating parts of that state that can mostly be
isolated, prefer child stores. You may have different blocks on a page that load progressively, or a settings drawer
on several pages that you may want to isolate from your main feature code.
- If you have differing lifecycles for some components of your logic, stores + composing via DI may be a good option.
- If your subscribers (e.g. UI) appear at different times and need different pieces of the state (e.g. progressive
content loading blocks), child stores allow you to subscribe to each one separately from the parent.
---
## Metrics Plugin
FlowMVI ships a metrics plugin that instruments the store (or plugin) pipeline and periodically
exports rich runtime statistics for intents, actions, states, subscriptions, lifecycle, and exception handling.
These metrics are designed to help you evaluate your stores' performance to find bottlenecks, slow loading,
excessive allocations or memory usage, excessive restarts, state updates, lock contention, subscription count,
excessive errors side effect count, as well as behavioral patterns from real-world users.
:::warning[Experimental]
This feature is experimental. Expect breaking changes in metrics schema.
:::
## Metrics collected
The schema is versioned via `MetricsSchemaVersion` and rendered through a `MetricSurface` for compatibility.
Each snapshot contains:
- **Intents** β totals, processed/dropped/undelivered counts, ops/sec, queue time, latency quantiles (p50/p90/p95/p99),
inter-arrival times, bursts, buffer occupancy, plugin overhead.
- **Actions** β sent/delivered/undelivered counts, ops/sec, delivery latency quantiles, queue time, buffer metrics,
plugin overhead.
- **State** β transition counts, vetoed transitions, started-in-initial-state, time-to-first state, reducer latency quantiles, throughput.
- **Subscriptions** β subscribe/unsubscribe events, current/peak subscribers, average/median lifetimes, sampled counts.
- **Lifecycle** β start/stop counters, total uptime, current/average/median lifetimes, bootstrap latency.
- **Exceptions** β total/handled counts, recovery latency (average/median).
- **Meta** β schema version, window length, EMA alpha, generated-at timestamp, start time, store name/id, run id.
Total: 67+ numeric metrics per snapshot.
## Usage guide
### 1. Add the dependency
```toml
flowmvi-metrics = { module = "pro.respawn.flowmvi:metrics", version.ref = "flowmvi" }
```
```kotlin
commonMainImplementation("pro.respawn.flowmvi:metrics:")
```
The artifact is lightweight and depends only on kotlinx.serialization.
### 2. Install the decorator
To collect metrics, you need a decorator, a plugin, and a sink:
```kotlin
val store = store(Initial) {
val metrics = collectMetrics(
reportingScope = applicationScope
)
reportMetrics(
metrics = metrics,
interval = 10.seconds,
sink = OtlpJsonSink(BackendSink()), // example
)
}
```
:::warning[reporting scope must outlive the store]
**Use a long-lived `reportingScope` (application/process/component scope) so metrics survive store restarts.**
Any metrics are only updated/emitted while the store is running to save resources,
but because stores can be restarted, collection happens on the `reportingScope` which must outlive the store
itself to ensure proper cleanup and not lose data on lifecycle changes.
:::
1. `collectMetrics` installs a [decorator](/plugins/decorators.md) that measures the store pipeline and returns
a `Metrics` implementation. This decorator returns you a `Metrics` instance that is attached to the store.
Keep one `Metrics` instance per store: the decorator will use it to push events and update the data you get when you
call `Metrics.snapshot()`. If you need to capture the latest data, you can use that interface to develop a custom
reporting logic.
- `offloadContext` moves computation/flushing off the main dispatcher. That's highly recommended to remove metric
collection overhead from your main store's logic.
- `windowSeconds` controls the sliding window for throughput.
- `emaAlpha` sets smoothing for [EMA](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)-based
averages.
2. `reportMetrics` installs a plugin that provides the default logic for metric flushing:
it snapshots the collector on a fixed interval and delivers the data to the `MetricsSink` you give it.
- Set `interval = Duration.INFINITE` to disable periodic snapshots and keep on-stop flushing only.
- If your sink is slow, the plugin will sacrifice reporting frequency by dropping the oldest snapshots without
sacrificing overall data integrity.
### 3. Implement a Sink that will send metrics
This part is on you. You need a place where you will send the metrics, such as a backend endpoint, log ingestion
infra, or a monitoring service.
To make your job easier, the library provides `Sink` decorators to format metrics for ingestion by:
- [OpenTelemetry](https://opentelemetry.io/),
- [Prometheus](https://prometheus.io/),
- [Open Metrics](https://openmetrics.io/),
- RESTful JSON endpoints.
- Plaintext/file-based loggers.
Built-in sinks:
- `LoggingJsonMetricsSink()` β serializes `MetricsSnapshot` to JSON and logs it via `StoreLogger` implementation.
- `OpenMetricsSink()` β emits [OpenMetrics](https://openmetrics.io/) text with `# EOF`, suitable for Prometheus HTTP
endpoints.
- `PrometheusSink` β same output without the EOF line (Prometheus 0.0.4 exposition).
- `OtlpJsonMetricsSink` β produces OTLP Metrics JSON ready for OpenTelemetry collectors.
- `ConsoleSink`, `StoreLoggerSink`, `AppendableStringSink`, `MappingSink`, `JsonSink`, and `NoopSink` building
blocks for quick wiring, tests, debug builds, or custom transport.
Pass `surfaceVersion` to downgrade emitted payloads for older consumers; otherwise the snapshotβs schema version is
used.
::::tip[Only release builds]
Reminder: you should only send metrics on release builds of your app to not pollute prod data.
::::
## Performance Overhead
You can find fresh benchmark results on CI and the source code in the `benchmarks` module.
In raw numbers, the results are (on a MacBook Pro M1 2021):
- Baseline: 0.342 Β± 0.003 us/10k intents (~813 ns/intent)
- With Metrics: 1.029 Β± 0.014 us/10k intents (~4382 ns/intent)
According to these, the overhead of metric collection is ~5.39x for a workflow with a single intent/state update path
compared to an identical configuration without metrics.
That looks like a big hit on paper, but in practice the hit is so small it's basically a rounding error.
With metrics enabled, you can still easily process 1000 intents in a single frame (16ms).
**Metrics becomes a meaningful CPU cost only if you process tens of thousands of
intents per second on a single hot path**
## Visualizing Metrics in the Debugger
FlowMVI's [Remote Debugger](/plugins/debugging.md) can display metrics collected from your stores in real-time.
This allows you to monitor store performance directly in the IDE plugin or desktop app without setting up
external monitoring infrastructure.
---
## Getting started with plugins
FlowMVI is built entirely based on Plugins!
Plugins form a chain of responsibility (called _Pipeline_) and
execute _in the order they were installed_ into the Store.
This allows you to assemble business logic like a lego by placing the "bricks" in the order you want, and transparently
inject some logic into any store at any point.
## Plugin Ordering
:::danger[The order of plugins matters!]
Changing the order of plugins may completely change how your store works.
Plugins can replace, veto, consume, or otherwise change anything in the store.
They can close the store or swallow exceptions!
:::
Consider the following:
```kotlin
val broken = store(Loading) {
reduce {
}
// β - logging plugin will not log any intents
// because they have been consumed by the reduce plugin
enableLogging()
}
val working = store(Loading) {
enableLogging()
reduce {
// β - logging plugin will get the intent before reduce() is run, and it does not consume the intent
}
}
```
That example was simple, but this rule can manifest in other, not so obvious ways. Consider the following:
```kotlin
val broken = store(Loading) {
serializeState() // βΌοΈ restores state on start
init {
updateState {
Loading // π€¦β and the state is immediately overwritten
}
}
// this happened because serializeState() uses onStart() under the hood, and init does too.
// Init is run after serializeState because it was installed later.
}
// or
val broken = store(Loading) {
install(customUndocumentedPlugin()) // βΌοΈ you don't know what this plugin does
reduce {
// β intents are not reduced because the plugin consumed them
}
init {
updateState {
// β states are not changed because the plugin veto'd the change
}
action(MyAction) // β actions are replaced with something else
}
}
```
So make sure to consider how your plugins affect the store's logic when using and writing them.
## Prebuilt Plugins
FlowMVI comes with a whole suite of prebuilt plugins to cover the most common development needs.
Here's a full list:
- **Reduce Plugin** - process incoming intents. Install with `reduce { }`.
- **Init Plugin** - do something when the store is launched. Install with `init { }`.
- **Recover Plugin** - handle exceptions, works for both plugins and jobs. Install with `recover { }`.
- **While Subscribed Plugin** - run jobs when the `N`th subscriber of a store appears. Install
with `whileSubscribed { }`.
- **Logging Plugin** - log events to a log stream of the target platform. Install with `enableLogging()`
- **Metrics Plugin** - capture performance metrics and export them as JSON, OpenMetrics/Prometheus, or OTLP.
Install with `collectMetrics()` and `reportMetrics()`. See [Metrics plugin](/plugins/metrics) for details.
- **Cache Plugin** - cache values in store's scope lazily and with the ability to suspend, binding them to the store's
lifecycle. Install with `val value by cache { }`
- **Async cache plugin** - like `cache`, but returns a `Deferred` that can be awaited. Advantageous because it does not
delay the store's startup sequence.
- **Job Manager Plugin** - keep track of long-running tasks, cancel and schedule them. Install with `manageJobs()`.
- **Await Subscribers Plugin** - let the store wait for a specified number of subscribers to appear before starting its
work. Install with `awaitSubscribers()`.
- **Undo/Redo Plugin** - undo and redo any action happening in the store. Install with `undoRedo()`.
- **Disallow Restart Plugin** - disallow restarting the store if you do not plan to reuse it.
Install with `disallowRestart()`.
- **Time Travel Plugin** - keep track of state changes, intents and actions happening in the store. Mostly used for
testing, debugging and when building other plugins. Install with `val timeTravel = timeTravel()`
- **Consume Intents Plugin** - permanently consume intents that reach this plugin's execution order. Install with
`consumeIntents()`.
- **Deinit Plugin** - run actions when the store is stopped.
- **Reset State Plugin** - reset the state of the store when it is stopped.
- **Saved State Plugin** - Save state somewhere else when it changes, and restore when the store starts.
See [saved state](/state/savedstate.md) for details.
- **Remote Debugging Plugin** - connect to the IDE Plugin / desktop app shipped with FlowMVI. See
the [documentation](/plugins/debugging.md) to learn how to set up the environment.
- **Metrics Plugin** - collect performance & usage metrics. See [docs](/plugins/metrics.md) for details.
- **Child Store Plugin** - attach other stores to the lifecycle of a parent. Learn more in
the [dedicated doc](/plugins/delegates.md)
- **Store Delegate Plugin** - delegate State and Actions of your store to another one. More
in [Composing Stores](/plugins/delegates.md)
- **Literally any plugin** - just call `install { }` and use the plugin's scope to hook up to store events.
All plugins are based on the essential callbacks that FlowMVI allows them to intercept.
The callbacks are explained on the [custom plugins](/plugins/custom.md) page.
Here's an explanation of how each default plugin works:
### Reduce Plugin
This is probably the most essential plugin in the library. Here's the full code of the plugin:
```kotlin
fun reducePlugin(
consume: Boolean = true,
name: String = ReducePluginName,
reduce: PipelineContext.(intent: I) -> Unit,
) = plugin {
this.name = name
onIntent {
reduce(it)
it.takeUnless { consume }
}
}
```
- This plugin simply executes `reduce` when it receives an intent.
- If you set `consume = true`, the plugin will **not** let other plugins installed after this one receive the intent.
Set `consume = false` to install more than one reduce plugin.
- By default, you can see above that this plugin must be unique. Provide a custom name if you want to have multiple.
Install this plugin in your stores by using
```kotlin
val store = store(Loading) {
reduce { intent ->
}
}
```
You don't need "Reducers" with FlowMVI. Reducer is nothing more than a function.
### Init plugin
This plugin invokes a given (suspending) action **before** the Store starts, each time it starts.
Here's the full code (simplified):
```kotlin
fun initPlugin(
block: suspend PipelineContext.() -> Unit,
) = plugin {
onStart(block)
}
```
Here are some interesting properties that apply to all plugins that use `onStart`:
- They are executed **each time** the store starts.
- They can suspend, and until **all** of them return, the store will **not handle any subscriptions, intents or any
other actions**
- They have a `PipelineContext` receiver which allows you to send intents, side effects and launch jobs
:::warning[Do not suspend forever]
Do not collect long-running flows or suspend forever in this plugin as it not only prevents the store from starting,
but also operates in the lifecycle of the store, which is active even if there are no subscribers (UI is not visible).
It does not respect system lifecycle and navigation backstack logic.
Consider using `whileSubscribed` if you need lifecycle awareness.
:::
This plugin can be useful when you want to do something **before** the store is fully started.
Install the init plugin by calling
```kotlin
val store = store(Loading) {
init { // this: PipelineContext
}
}
```
### Recover plugin
Here's the full code of the plugin:
```kotlin
fun recoverPlugin(
name: String? = null,
recover: PipelineContext.(e: Exception) -> Exception?
) = plugin {
this.name = name
onException(recover)
}
```
This plugins executes `recover` lambda each time an exception happens in any of the store's callbacks, plugins or jobs
This callback is invoked asynchronously **after** the exception has been thrown and the job that threw it was cancelled.
With this plugin, you cannot continue the execution of the job because it has already ended.
If you return `null` from this plugin, this means that the exception was handled and it will be swallowed in this case.
This plugin can be useful to display an error message to the user, retry an operation, or report errors to analytics.
Install this plugin by using:
```kotlin
val store = store(Loading) {
recover { e: Exception ->
null
}
}
```
### While Subscribed Plugin
This plugin launches a background job whenever the number of store subscribers reaches a minimum value (1 by default)
and automatically cancels it when that number drops below the minimum.
```kotlin
fun whileSubscribedPlugin(
minSubscriptions: Int = 1,
stopDelay: Duration = 1.seconds,
name: String? = null,
block: suspend PipelineContext.() -> Unit,
) = plugin {
onStart {
launch {
doWhileSubscribed(block)
}
}
}
```
This plugin is useful for starting and stopping observation of some external data sources when the user can interact
with the app. For example, you may want to collect some flows and call `updateState` on each emission to update
the state you display to the user.
1. After the store is started, this plugin will begin receiving subscription events from the store.
2. The **first time** the number of plugins reaches the minimum, the block that you provided will be run.
3. The job will stay active until it either ends by itself or the number of subscriptions drops below the minimum.
4. Once there are less than `minSubscriptions` subscribers, the plugin will wait `stopDelay` and then cancel `block`.
5. If the job has ended by itself, it will only be launched **after** the count of subscriptions has dropped below the
minimum. I.e. it will not be relaunched each time an additional subscriber appears,
but only when the condition is satisfied the next time again.
:::info[Suspend in the body]
This plugin expects you to suspend inside `block` because it already launches a background job.
You can safely collect flows and suspend forever in the `block`, and the job will follow the subscription count.
:::
Install the plugin with:
```kotlin
val store = store(Loading) {
whileSubscribed {
}
}
```
### Logging plugin
This plugin prints the events that happen in the store to the `logger` that you specified when you were creating the
store.
The default `PlatformStoreLogger` will print to:
- Logcat on Android
- NSLog on Apple platforms
- Console on Wasm and JS
- Stdout / Stderr on JVM
- Stdout on other platforms
---
- Tags are only used on Android, so on other platforms they will be appended as a part of the message.
- On platforms that do not support levels, an emoji will be printed instead
- Don't worry about heavy operations inside your `log { }` statements, the lambda is skipped if there is no logger.
- Use `NoOpStoreLogger` if you want to prevent any kind of logging, for example on production.
Install this plugin with:
```kotlin
val store = store(Loading) {
enableLogging()
}
```
### Cache / Async Cache Plugins
Here's a simplified version of the code:
```kotlin
fun cachePlugin(
init: suspend PipelineContext.() -> T,
) = plugin {
val value = CachedValue(init)
onStart { value.init() }
onStop { value.clear() }
}
```
This plugin provides a delegate that is very similar to `lazy`, but the reference that the plugin holds is tied to the
lifecycle of the store, which means when the store starts, the value is initialized using the provided `init` parameter,
and when the store stops, it clears the reference to the value. If you use the `PipelineContext` inside, it will be
cancelled by the store itself.
By default, the entire store startup sequence will suspend until all values are initialized, but if you don't want that,
there is a second version of this plugin called `asyncCache` that returns a `Deferred` you can await. This one can
be very useful to initialize a lot of heavy stuff in parallel.
- You can create a `CachedValue` outside of the store if you need to access it outside of the store builder scope,
but you **must** install the plugin using the value, and you must **not** try to access the value outside of the
store's lifecycle, or the attempt will throw. To create it, use the `cached { }` delegate.
- You can access the value returned by `cache` in the `onStop` callback because the `onStop` is called in reverse plugin
installation order.
This plugin is most useful:
- When you want to either suspend in the initializer (like a suspending `lazy`), in which case it will function
similarly to `init` plugin
- When you want to use the `PipelineContext` (and its `CoroutineScope`) when initializing a value, for example with
pagination or shared flows
Install this plugin using:
```kotlin
suspend fun produceTimer(): Flow
val store = store(Loading) {
val timer by cache {
produceTimer().stateIn(scope = this, initial = 0)
}
}
```
or provide the value externally:
```kotlin
// do not access outside the store lifecycle
// need to specify type parameters - ambiguous
val value = cached<_, State, Intent, Action> { produceTimer() }
val store = store(Loading) {
install(cachePlugin(value))
}
```
### Job Manager Plugin
FlowMVI provides a `JobManager` class that can store references to long-running `Job`s by an arbitrary key and manage
them. Job manager can then hook up to the store lifecycle events to cancel the jobs as appropriate:
```kotlin
fun jobManagerPlugin(
manager: JobManager,
name: String? = JobManager.Name,
) = plugin {
this.name = name
onStop { manager.cancelAll() }
}
```
Examine the methods of the `JobManager` class to learn what it can do.
Create a job manager and immediately install it using:
```kotlin
enum class Jobs { Connection }
val store = store(Loading) {
val jobs: JobManager = manageJobs()
}
```
Or provide the job manager externally:
```kotlin
val manager = JobManager()
val store = store(Loading) {
manageJobs(manager)
}
```
Then register a job once you launch it:
```kotlin
val store = store(Loading) {
val jobs = manageJobs()
init {
launch {
websocket.connect()
}.registerOrReplace(Jobs.Connection, jobs)
}
recover { e ->
if (e is DeviceOfflineException) jobs.cancel(Connection)
e
}
}
```
### Await Subscribers Plugin
This plugin allows you to suspend until the store has reached a specified number of subscribers present.
To use it, you can create an instance of `SubscriberManager` and call `await` to suspend until the condition is met.
```kotlin
fun awaitSubscribersPlugin(
manager: SubscriberManager,
minSubs: Int = 1,
allowResubscription: Boolean = true,
suspendStore: Boolean = true,
timeout: Duration = Duration.INFINITE,
name: String = SubscriberManager.Name,
) = plugin {
/* ... */
}
```
- Specify `minSubs` to determine the minimum number of subscribers to reach.
- Choose `suspendStore` to block all store operations until the condition is met. If you pass `false`, only the code
that explicitly calls `await()` will suspend.
- If you pass the `allowResubscription` parameter, then after they leave, the state will reset and you
can call `await()` again.
- Specify a `timeout` duration or `complete()` the job manually if you want to finish early.
### Undo/Redo Plugin
Undo/Redo plugin allows you to create and manage a queue of operations that can be undone and repeated.
```kotlin
fun undoRedoPlugin(
undoRedo: UndoRedo,
name: String? = null,
resetOnException: Boolean = true,
) = plugin {
this.name = name
onStop { undoRedo.reset() }
if (resetOnException) onException {
it.also { undoRedo.reset() }
}
}
```
The events will be reset when store is stopped and (optionally) when an exception occurs.
You can observe the queue of events for example in `whileSubscribed`:
```kotlin
val store = store(Loading) {
val undoRedo = undoRedo(queueSize = 10)
whileSubscribed {
undoRedo.queue.onEach { (i, canUndo, canRedo) ->
updateState {
copy(index = i, canUndo = canUndo, canRedo = canRedo)
}
}.collect()
}
reduce { intent ->
when (intent) {
is ClickedRedo -> undoRedo.redo()
is ClickedUndo -> undoRedo.undo()
is ChangedInput -> undoRedo(
redo = { updateState { copy(input = intent.current) } },
undo = { updateState { copy(input = intent.previous) } },
)
}
}
}
```
This plugin can be useful whenever you are implementing an "editor" type functionality, but currently not fully
implemented to handle all edge cases.
### Time Travel plugin
Time travel records all Intents, Actions, State Changes, subscription events, starts and stops of the store.
It's mostly useful for debugging, logging and other technical tasks.
For example, FlowMVI's testing DSL embeds a time travel plugin when testing the store.
```kotlin
fun timeTravelPlugin(
timeTravel: TimeTravel,
name: String = TimeTravel.Name,
) = plugin {
this.name = name
/* ... */
}
```
Install the plugin using:
```kotlin
val store = store(Loading) {
val timeTravel = timeTravel()
init {
assert(timeTravel.starts == 1)
}
}
```
### Deinit plugin
This one is a simple DSL for calling `onStop`:
```kotlin
fun deinitPlugin(
block: ShutdownContext.(e: Exception?) -> Unit
) = plugin { onStop { block(it) } }
```
It is useful in combination with `cache` plugin, or if you need to clean up some external resource or set the state
as the store stops. It is called reliably (but synchronously) on store shutdown.
The exception will be `null` on normal shutdowns, and non-null when there was an error, just before the store throws.
Install it by simply calling `deinit { }`.
### Reset State Plugin
```kotlin
fun resetStatePlugin() = plugin {
this.name = "ResetStatePlugin"
onStop {
updateStateImmediate { config.initial }
}
}
```
This plugin simply resets state back to `initial` once the store is stopped.
The moment when this happens is determined by the plugin installation order. Install with `resetStateOnStop()`
### Or Create Your Own
As if having so many plugins was not great in itself, the true power of the library is in creating custom plugins.
Learn how to do that in the [next guide](/plugins/custom.md)
---
## Persist and Restore State
The `savedstate` artifact contains plugins and API necessary to save and restore the state of a store to a place
that outlives its lifespan. This is useful in many cases and provides an unparalleled UX. For example, a person may
leave the app while the form they were filling is unfinished, then return to the app and see all of their data
being restored, continuing their work.
## 1. Adding a dependency
```toml
flowmvi-savedstate = { module = "pro.respawn.flowmvi:savedstate", version.ref = "flowmvi" }
```
The artifact depends on:
- `kotlinx-io`, and as a consequence, on `okio`
- `kotlinx-serialization`, including Json
- `androidx-lifecycle-savedstate` on Android to parcelize the state.
The artifact depends on quite a few things, so it would be best to avoid adding it to all of your modules.
Instead, you can inject the plugin or savers using DI in [this guide](/plugins/debugging.md).
## 2. Defining `Saver`s
The basic building block of the module is the `Saver` interface/function. Saver defines **how** to save the state.
Use the `Saver` function to build a saver or implement the
interface to write your custom saving logic, or use one of the prebuilt ones:
- `MapSaver` for saving partial data.
- `TypedSaver` for saving a state of a particular subtype.
- `JsonSaver` for saving the state as a JSON.
- `FileSaver` for saving the state to a file. See `DefaultFileSaver` for custom file writing logic.
- `CompressedFileSaver` for saving the state to a file and compressing it.
- `NoOpSaver` for testing.
`Saver`s can be decorated and extended. For example, you can build a saver chain to store a particular type of the state
in a compressed Json file:
```kotlin
val saver = TypedSaver(
JsonSaver(
json = Json,
serializer = DisplayingCounter.serializer(),
delegate = CompressedFileSaver { path },
)
)
```
You can invoke the `save` method of the saver manually if you keep a reference to it.
## 3. Choosing `SaveBehavior`
For now there are two types of behaviors that you can use to decide **when** to save the state.
### `OnChange`
This behavior will save the state each time it is changed and after a specified `delay`.
If the state changes before or during the operation of saving the state, the delay will be restarted and the previous
job will be canceled.
In general, don't use multiple values of this behavior, because only the minimum delay value will be respected.
### `OnUnsubscribe`
This behavior will persist the state when a subscriber is removed and the store is left with a specified number of
`remainingSubscribers`.
This will happen, for example, when the app goes into the background.
Don't use multiple instances of this behavior, as only the maximum number of subscribers will be respected.
### `Periodic`
Save the state periodically after the specified `delay` regardless of whether the state was updated.
This is useful when you are updating the state with `updateStateImmediate`, e.g. text fields.
:::info
By default, `OnChange` and `OnUnsubscribe` are used - on each change, with a sensible delay, and when all subscribers leave.
You can customize this via the `behaviors` parameter of the plugin.
:::
## 4. Installing the plugin
To start saving the state, just install your preferred variation of the `saveState` plugin:
### Custom state savers
```kotlin
val store = store(initial = Loading) { // start with a default loading value as we still need it
saveState(
saver = CustomSaver(),
context = Dispatchers.IO,
resetOnException = true, // or false if you're brave
)
}
```
### Serializing state to a file
You don't have to define your own savers if you don't need to. There is an overload of the `saveStatePlugin` that
provides sensible defaults for you, called `serializeState`:
```kotlin
serializeState(
path = path, // (1)
serializer = DisplayingCounter.serializer(), // (2)
recover = NullRecover // (3)
)
```
1. Provide a path where the state will be saved.
- It's best to use a subdirectory of your cache dir to prevent it from being fiddled with by other code.
- On web platforms, the state will be saved to local storage.
2. Mark your state class as `@Serializable` to generate a serializer for it.
- It's best to store only a particular subset of states of the Store because you don't want to restore the user
to an error / loading state, do you?
3. Provide a way for the plugin to recover from errors when parsing, saving or reading the state. The bare minimum
is to ignore all errors and not restore or save anything, but a better solution like logging the errors can be used
instead. By default, the plugin will just throw and let the store (`recoverPlugin`) handle the exception.
### Storing the state in a bundle
If you're on android, there is the `parcelizeState` plugin that will store the state in a `SavedStateHandle`:
```kotlin
parcelizeState(
handle = savedStateHandle,
key = "CounterState",
)
```
- The `key` parameter will be derived from the Store / class name if you don't specify it, but watch out for conflicts!
- This plugin uses the `ParcelableSaver` by default, which you can use too.
:::warning
Watch out for parcel size overflow exceptions! The library will not check the resulting parcel size for you.
:::
:::info
According to the documentation, any writes to your saved state will only be restored if the app
was killed by the OS. This means you will not see any state restoration results unless the OS kills the activity
itself (i.e. exiting the app will not result in the state being restored).
:::
## 5. Caveats
### App updates
Saving state is great, but think about what will happen to your app when the app is updated and the resulting state
structure changes. For example, the name of the property may stay the same but its meaning may have changed.
This means, when the state will be restored, unpredictable behavior may occur. This does not necessarily mean
restoration will fail, but that the logic may be affected. On Android, the system will clear the saved state for you,
but if you persist the state to a file, you have to keep track of this yourself.
The best way to solve this would be to clear the saved state (for example, by deleting the directory) on each app
update.
You can do this by registering a broadcast receiver or checking if the app was updated upon startup.
Implementation of this logic is out of scope of the library.
### Crashes and failures
If the app fails for whatever reason, it's important that that may invalidate the state or even result in further
crashes. If your app crashes, make sure to invalidate the saved state as well, for example, by using Crashlytics,
overriding the main looper to catch fatal exceptions, or any other means.
The library will clear the state when an exception happens in the store and can let you recover from errors, but
that is not enough as crashes may happen in other places in your app, such as the UI layer.
### Saving sensitive data
If you save the state, you have to think about excluding sensitive data such as passwords and phone numbers
from it. Annotate the serializable state fields with `@Transient`, for example, or create a subset of the
state properties that you will save.
Unless you implement savers that encrypt the data, ensure the safety of the user by not storing sensitive data at all.
---
## Managing State
State management in FlowMVI is slightly different from what we are used to in MVVM.
This guide will teach you everything about how to manage application state in a fast and durable manner.
## Understanding State
In FlowMVI, state is represented by classes implementing the `MVIState` marker interface.
The simplest state looks like this:
```kotlin
data class CounterState(
val counter: Int = 0,
val isLoading: Boolean = false
) : MVIState
```
States must be:
- **Immutable** - State object must never change after it is created
- **Comparable** - State object must implement a stable and valid `hashCode`/`equals` contract
- **Scoped** - Unintended objects (like 3rd party interfaces or network responses) should not be used as a State.
The marker interface `MVIState` is needed to **enforce** the requirements above at compilation time.
To adhere to the requirements above, the only thing you need to do in most cases is to use `data class`es or `data object`s that will generate `equals` and `hashCode` for you.
:::warning[Do not mutate the state directly!]
Avoid mutable properties like `var` or mutable collections.
Always create new instances using `copy()` and ensure the collections you pass are **new** ones, not just `Mutable` collections upcasted to their parent type,
otherwise your state updates will **not be reflected**.
:::
:::tip[Empty state]
If your store does not have a `State`, you can use an `EmptyState` object provided by the library.
:::
## State Families
The state in the example above can be used by itself, but most apps can have more than one state. The best example is LCE (loading-content-error) state family.
The key things we want from such state are that:
- There are no unused/junk/placeholder values for each state.
- For example, during an `Error` state, there is no data because it failed loading, and vice versa, when the state is `Content`, we don't have an error to report.
- State clients who use it cannot gain access to unwanted data.
- For example, if we pass our state to the UI to render it, we want to avoid accidentally showing **both** an error message **and** the list of items.
To achieve our goals above, FlowMVI supports **State Families**, represented as sealed interfaces:
```kotlin
sealed interface LCEState : MVIState {
data object Loading : LCEState
data class Error(val e: Exception) : LCEState
data class Content(val items: List) : LCEState
}
```
:::tip[Why an interface?]
Using `sealed interface` instead of `class` improves performance by reducing allocations and prevents state classes from having any logic (private members).
:::
However, the code above introduces some complexity to handling state types, such as needing to cast or check the state's type before updating it:
```kotlin
val current = state.value as? Content ?: return
// use the property
val items = current.items
```
For that, the library provides a DSL consisting of two functions:
```kotlin
// capture and update
updateState { // this: LCEState.Content
copy(items = items + loadMoreItems())
}
// capture but do not change
withState { // this: LCEState.Error
action(ShowErrorMessage(exception = this.e))
}
```
These functions first check the type of the state, and if it is not of the first type parameter, they skip the operation inside the `block` entirely.
:::tip[Fail Fast]
If you want to throw an exception instead of skipping the operation, there are `updateStateOrThrow` / `withStateOrThrow` functions.
:::
Using the functions above not only simplifies our code but also prevents various bugs due to the asynchronous nature of state updates, such as the user spamming buttons during
an animation, leading to, for example, the app retrying failed data loading multiple times.
### Nested State Families
Of course, you can mix and match the approaches above or introduce multiple nesting levels to your states.
For example, to implement progressive content loading, you can create a common state `data class` with multiple families nested inside:
```kotlin
sealed interface FeedState: MVIState {
data object Loading: FeedState
data class Content(val items: List): FeedState
}
// implementing `MVIState` for nested states is not required but beneficial
sealed interface RecommendationsState: MVIState { /* ... */ }
data class ProgressiveState(
val feed: FeedState = FeedState.Loading,
val recommended: RecommendationsState = RecommendationsState.Loading,
/* ... */
): MVIState
```
In that case, you will not need the typed versions of `updateState`, but rather want to use two other functions provided by the library:
- `value.typed()` to cast the `value` to type `T` or return `null` otherwise (just like the operator `as?`)
- `value.withType { }` to operate on `value` only if it's of type `T`, or just return it otherwise
If you represent the state this way, you will never have to write `null`-ridden code again to manage states with placeholders,
and your stores' subscribers will never have the problem of rendering an inconsistent or invalid state.
Next, let's talk about state updates.
## Serialized State Transactions (SSTs)
The key difference that FlowMVI has over conventional approaches is that state transactions (changes) are **serialized**.
This has nothing to do with JSON or networking, but rather, the term comes from the [Database Architecture]().
In simple terms:
> Store's `state` is changed **sequentially** to prevent data races.
Consider the following:
```kotlin
val state = MutableStateFlow(State(items = emptyList()))
suspend fun loadMoreItems() {
val lastIndex = state.value.items.lastIndex // (1)
val newItems = repository.requestItems(atIndex = lastIndex, amount = 20)
state.value = state.value.copy(items = items + newItems) // (2)
}
```
This code contains multiple race conditions:
1. The code obtains the index to the last item to load more items, then executes a long operation, during which **the state may have already changed**.
When trying to add new items, the item list could have already been modified, and we'll get duplicate or stale values appended. This is a **data race**.
2. When mutating the state using `state.value`, while the right-hand side of the expression is being evaluated, the left-hand side
(`state.value`) could have already been changed by another thread, leading to the right-hand side overwriting the state with stale data. This is a **thread race**.
These problems arise only when we need to **read the current state** to make a decision on what to do next.
In the example above, we need the current items to load the next page, but this is apparent in many other cases as well,
such as form validation or any conditional logic.
You may have never encountered these issues before, but this is only due to luck - because the state was modified on the main thread and sequentially.
FlowMVI, unlike many other libraries, allows the state to be modified on **any thread** to enable much better performance and reactiveness of your apps.
You **will** run into these issues as soon as you override `coroutineContext` or enable the `parallelIntents` property of the Store.
Regarding the `StateFlow.update { }` operator
To address a common objection to this argument that sounds like:
> But I can just read the state in the `update` block because it's thread-safe!
Study the [documentation](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-state-flow/#1996476265%2FFunctions%2F1975948010) of the update function:
> `function` may be evaluated multiple times, if `value` is being concurrently updated.
This means that if the starting and ending states do not match, your `function` block will be executed **multiple times**.
This not only wastes resources but can be detrimental if you call a function that is not _idempotent_ (has side effects) inside the `function` block.
For example, if you are making a banking app and perform a transaction, then the code above can lead to the user's credit card being charged multiple times.
Even if you always modify the state on the main thread, or use `update { }`, the parallel nature of coroutines does not prevent you from having data races.
---
To combat the problems above, the library uses SSTs by default when you use the state.
This is also why you can't just access `state` wherever you wish.
Instead, you only have 2 functions to gain access to the state: `updateState` and `withState`.
The typed functions we discussed in the previous section are just aliases to the more generic ones, and they work by making sure that while the `block` is executing,
**no one else can read or change the state**. Whenever you call `updateState`, the transaction is synchronized using a Mutex, and all other code that tries to also
call `updateState` will wait in a FIFO queue until the `block` finishes execution and the state is updated.
This means that when you have access to the state, you can be sure it will not change to something unexpected inside the lambda.
You can execute any suspending and long-running code inside the block and it will only be executed **once** and on the most up-to-date value.
:::info
When you use `updateState`, your store's plugins will receive both the initial and the resulting value, so that they can also respond to the update or modify it.
This does not happen when `withState` is called as the state does not change.
:::
### Reentrant vs Non-Reentrant
By default, SSTs are **reentrant**, which means that you are allowed to write code like this:
```kotlin
updateState {
updateState {
}
}
```
In that case, if you are already inside a state transaction, a new one will **not be created** for nested update blocks.
Otherwise, you would get a **deadlock** as the inner block waits for the outer update to finish, which waits for the inner update to finish.
This, unfortunately, has a performance penalty due to context switching, which makes reentrant transactions ~1600% slower.
For most apps, however, this is negligible as the time is still measured in microseconds.
When configuring a Store, it is possible to change the `stateStrategy` property to make the transactions non-reentrant:
```kotlin
val store = store(initial = Loading) {
configure {
stateStrategy = Atomic(reentrant = false)
}
}
```
In this case, while the store is `debuggable`, the library will check the transaction for you so that instead of a deadlock you at least get a crash.
:::info[New default in 4.0]
In the future, non-reentrant transactions may become the default for the simple reason of redundancy.
Since you are in the transaction, can just use `this` property, which is already the most up-to-date state.
:::
### Bypassing SSTs
Although non-reentrant transactions are already very fast, they are still ~2x slower due to locking overhead.
Only if you absolutely **must** squeeze the last drop of performance from the Store, and you are **sure** you handle the problems discussed above,
you can use one of two ways to override the synchronization:
- `updateStateImmediate` function, which avoids all locks, **plugins** and thread safety, or
- `StateStrategy.Immediate` which disables SSTs entirely.
:::danger
`updateStateImmediate` **bypasses all plugins** in addition to lacking thread safety!
:::
Where can bypassing be needed?
One example where overriding is needed is Compose's text fields:
```kotlin
data class State(val input: String = "") : MVIState
@Composable
fun IntentReceiver.ScreenContent(state: State) {
TextField(
value = state.input,
onValueChange = { intent(ChangedInput(it)) },
)
}
val store = store(State()) {
reduce { intent ->
when(intent) {
is ChangedInput -> updateStateImmediate {
copy(input = intent.value)
}
}
}
}
```
Due to flaws in Compose's `TextField` implementation, if you do not update the state immediately, the UI will have jank.
This will be addressed in future Compose releases with `BasicTextField2`.
:::warning[Do not leak the state]
It's still possible to leak the state by assigning it to external variables or launching coroutines while in an SST.
This can be necessary, but if you do leak the state, always assume that **any** state outside the transaction is **invalid and outdated**.
:::
## Reactive State Management
With MVVM, a best practice is to produce the state from several upstream flows using `combine`, then the `stateIn` operator to make the flow hot.
A key distinction of MVI compared to MVVM is that a Store always has a single, hot, mutable state.
To avoid resource leaks and redundant work, the state should only be updated **while the subscribers are present**.
FlowMVI provides the API for that in the form of the `whileSubscribed` plugin:
```kotlin
val store = store(ProgressiveState()) { // initial value just like stateIn
whileSubscribed {
combine(
repo.getFeedFlow(),
repo.getRecommendationsFlow(),
) { feed, recommendations ->
updateState {
copy(
feed = FeedState.Content(feed),
recommended = RecommendationsState.Content(recommendations),
)
}
// don't forget to collect the flow
// highlight-next-line
}.consume(Dispatchers.Default)
}
}
```
### Persisting data
Additionally, whenever you produce your state, such as in the `combine` lambda, you must **consider the current state**.
For example, if your state has an in-memory value, such as a text input, and you use a State Family, you must "persist" the previous value so that it is not overridden:
```kotlin
sealed interface State : MVIState {
data object Loading : State
data class Content(
val items: List,
val searchQuery: String = "", // in-memory value
) : State
}
val store = store(State.Loading) {
whileSubscribed {
repo.getItems().collect { items ->
updateState {
State.Content(
items = items,
// highlight-next-line
searchQuery = typed()?.searchQuery ?: "" // preserve the input
)
}
}
}
}
```
In the code above, we use the `typed` function to check the type of the previous state, and if it was already `Content`, we preserve the `searchQuery` value.
The framework has no preference over whether to keep a separate flow like in MVVM, or to keep the value in the state directly,
but the state-based approach has the advantage of using SSTs and state families to achieve greater safety.
If your concern is the boilerplate, you can extract your in-memory data into a separate `data class`, which only needs one type-check to preserve.