# 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 ![Maven Central](https://img.shields.io/maven-central/v/pro.respawn.flowmvi/core?label=Maven%20Central) ```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 ![FlowMVI Framework Banner](https://opensource.respawn.pro/FlowMVI/banner.webp) [![CI](https://github.com/respawn-app/FlowMVI/actions/workflows/ci.yml/badge.svg)](https://github.com/respawn-app/FlowMVI/actions/workflows/ci.yml) ![License](https://img.shields.io/github/license/respawn-app/flowMVI) ![GitHub last commit](https://img.shields.io/github/last-commit/respawn-app/FlowMVI) ![Issues](https://img.shields.io/github/issues/respawn-app/FlowMVI) ![GitHub top language](https://img.shields.io/github/languages/top/respawn-app/flowMVI) [![AndroidWeekly #563](https://androidweekly.net/issues/issue-563/badge)](https://androidweekly.net/issues/issue-563/) [![Slack channel](https://img.shields.io/badge/Chat-Slack-orange.svg?style=flat&logo=slack)](https://kotlinlang.slack.com/messages/flowmvi/) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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: [![Quickstart](https://img.shields.io/website?down_color=red&down_message=Offline&label=Quickstart&up_color=green&up_message=Online&url=https%3A%2F%2Fopensource.respawn.pro%2FFlowMVI)](https://opensource.respawn.pro/FlowMVI/quickstart) * Latest version: [![Maven Central](https://img.shields.io/maven-central/v/pro.respawn.flowmvi/core?label=Maven%20Central)](https://central.sonatype.com/namespace/pro.respawn.flowmvi) * API Docs: [![Javadoc](https://javadoc.io/badge2/pro.respawn.flowmvi/core/javadoc.svg)](https://opensource.respawn.pro/FlowMVI/javadocs/index.html) * Sample App + Showcase (Web): [![Sample](https://img.shields.io/badge/Click_Me-Click_Me?style=flat&color=00b147)](https://opensource.respawn.pro/FlowMVI/sample/) * Ask questions on [![Slack](https://img.shields.io/badge/Chat-Slack-orange.svg?style=flat&logo=slack)](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: [![Plugin](https://img.shields.io/jetbrains/plugin/v/25766?style=flat)](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 ![Maven Central](https://img.shields.io/maven-central/v/pro.respawn.flowmvi/core?label=Maven%20Central) ```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.2 192.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. ![setup.png](/debugger_setup_1.png) 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.