# FlowRedux: a state machine DSL built on Kotlin coroutines and Flow

> FlowRedux is a Kotlin Multiplatform library for building asynchronous state machines with a DSL layered over the coroutines Flow type, where states, actions and transitions are declared in a spec block rather than in a reducer function. It ships for the JVM, Android, iOS, watchOS, tvOS, macOS, Linux and Windows, and JavaScript support is planned but not yet available.

**freeletics/FlowRedux** — Kotlin Multiplatform Statemachine library with nice DSL based on Flow from Kotlin Coroutine's.

- Repository: https://github.com/freeletics/FlowRedux
- Website: https://freeletics.github.io/FlowRedux/
- Stars: 785 · Forks: 26
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/freeletics-flowredux

## What the library actually models

FlowRedux is described in the README as an async running Kotlin Multiplatform state machine made easy with a DSL and coroutines, and the repository description calls it a Kotlin Multiplatform statemachine library with a nice DSL based on Flow from Kotlin Coroutines.

The design is a state machine with a single stream of state and a single stream of actions. A state machine is declared as a class extending FlowReduxStateMachine with a type parameter for the state and another for the action, and it exposes a state property that is a Flow, so a consumer collects the state and a producer dispatches actions.

The DSL is the spec block in the initialiser, and inside it you declare which state you are in and what happens there. The example declares a sealed interface State with a Loading object, a ContentState data class and an Error data class, and a sealed interface Action with a RetryLoadingAction object.

The interesting part is that the spec is a set of blocks keyed by state, and within a state you attach handlers. The inState<Loading> block has an onEnter handler, which runs whenever the machine enters that state. The inState<Error> block has an on handler for a specific action. The inState<ContentState> block has a collectWhileInState handler that observes a flow only for as long as the machine stays in that state.

So the machine is described by where it is and what it does there, rather than by a single reduce function with a switch statement. That is the whole idea in one sentence.

## Transitions, mutations and the three ways to change state

The example code shows three distinct ways of affecting the state, and they are worth separating because they have different semantics.

The first is the override, used in the onEnter handler of the Loading state.

```kotlin
try {
    val items = loadItems() // suspending function / coroutine to load items
    state.override { ContentState(items) } // Transition to ContentState
} catch (t : Throwable) {
    state.override { Error(t) } // Transition to Error state
}
```

An override replaces the state outright, which is a transition to a different state. Because the handler is an onEnter for Loading, this is what runs when the machine enters that state, and the try and catch around a suspending call is the natural place to handle a failure by transitioning to the error state.

The second is the mutate, used in the collectWhileInState handler of the ContentState.

```kotlin
state.mutate {
    copy( items = this.items + Item("New item $value"))
}
```

A mutation changes the current state in place using the copy semantics of a data class, so the state type stays the same and only the data changes. The distinction between override and mutate is the distinction between a state change and a state update, and having them as separate operations means the machine can tell them apart.

The third is the action handler, where a specific action is dispatched.

```kotlin
on<RetryLoadingAction> { action : RetryLoadingAction, state : State<Error> ->
    state.override { Loading } // Transition to Loading state which loads list again
}
```

So the machine has a single transition point for leaving a state and a single mechanism for entering one, which is what makes the flow of a screen readable as a diagram rather than as a pile of branches.

## Collecting state, dispatching actions and the Android ViewModel case

Consuming the machine is the short part, and it is the same shape as any Flow.

```kotlin
val statemachine = MyStateMachine()

launch {  // Launch a coroutine
  statemachine.state.collect { state ->
    // do something with new state like update UI
    renderUI(state)
  }
}

// emit an Action
launch { // Launch a coroutine
  statemachine.dispatch(action)
}
```

Two coroutines, one collecting the state and one dispatching actions, and the state is immutable from the consumer's point of view.

The Android case shows the pattern with AndroidX ViewModel, and the reason the library exists in an Android codebase is visible here. The ViewModel holds the state machine, collects its state into a MutableLiveData, and dispatches actions from a viewModelScope launch.

```kotlin
class MyViewModel @Inject constructor(private val stateMachine : MyStateMachine) : ViewModel() {
    val state = MutableLiveData<State>()

    init {
        viewModelScope.launch { // automatically canceled once ViewModel lifecycle reached destroyed.
            stateMachine.state.collect { newState ->
                state.value = newState
            }
        }
    }

    fun dispatch(action : Action) {
        viewModelScope.launch {
            stateMachine.dispatch(action)
        }
    }
}
```

The comment on the viewModelScope launch is the point of the whole arrangement. The scope is automatically canceled once the ViewModel lifecycle reaches destroyed, so the state machine's coroutines do not outlive the screen, and the machine itself is injected rather than created inside the ViewModel, which means it can be shared or tested independently of the Android lifecycle.

The topics name the intended architecture directly, with android, architecture, coroutines, mvi and mvi-android among them, and the library is from the Freeletics organisation, which is a company whose published work on MVI and state machines in Kotlin is well known.

## The two artifacts, the platform matrix and the snapshots

The dependency section is short and worth reading carefully, because the choice between artifacts and platforms is a real decision.

There are two artifacts. The flowredux artifact is the core library and includes the DSL. The compose artifact contains convenient extensions for working with FlowReduxStateMachine in Jetpack Compose.

For a JVM or Android only project, the coordinates are separate, with a jvm suffix on the core artifact and a separate compose one.

```groovy
implementation 'com.freeletics.flowredux:flowredux-jvm:<latest-version>'
implementation 'com.freeletics.flowredux:compose:<latest-version>'
```

For a multiplatform project, a single artifact covers it.

```groovy
implementation 'com.freeletics.flowredux:flowredux:<latest-version>'
```

The supported platforms are listed as JVM and Android, iOS, watchOS, tvOS, macOS, Linux and Windows. And then the honest sentence: we do plan to add support for JavaScript but it's not available yet.

That is worth knowing before you commit, because a Kotlin Multiplatform project that also targets the web has a gap in its platform matrix, and the alternative is maintaining a separate state machine implementation for the web target.

Snapshots are published directly from the main branch from CI on each change, which means the snapshot repository has to be added to your Gradle repositories block, and the version is then used with a -SNAPSHOT suffix.

## Where FlowRedux is the wrong choice

The first limitation is that it is a large idea to adopt for a small problem. A screen with one button and one boolean does not need a state machine DSL, and a sealed class of events plus a reduce function plus a StateFlow is a hundred lines of code with no new dependency and no DSL to learn. The library pays off when the number of states and the number of transitions make the reducer hard to read, and that is a real threshold rather than a preference.

The second is the learning curve of the DSL itself. The spec block is not a language most Kotlin developers have met, and the interaction between the state parameter passed to a handler and the override and mutate operations is a specific vocabulary. The README directs readers to a full documentation and best practices page, and best practices pages exist for a reason.

The third is the JavaScript gap, which for a multiplatform project is a blocker rather than an inconvenience. The README says support is planned but not available yet, so a project with a web target either excludes the library there or ships a second implementation of the same state machine.

The fourth is that the state machine owns the coroutine scope question, and getting that right is part of using the library rather than free. The ViewModel example works because the scope is cancelled with the ViewModel, and in a non-Android multiplatform project the equivalent has to be supplied by the application, which is a decision the library cannot make for you.

The fifth is the release churn. The recent releases are 2.1.1 from 2026-06-24, 2.1.0 from 2026-06-24 and 2.0.1 from 2026-01-12, so two releases on the same day in the 2.1 line and a 2.0 in between.

## FlowRedux compared with a hand written reducer and with Turbine

The comparison with a hand written reducer is the baseline every Kotlin developer already has, and the difference in approach is where the state transition logic lives. A reducer puts the transitions in a function you call from a view model, and the states are plain data classes. FlowRedux puts the transitions in a declarative spec where the transitions out of a state are grouped with the state they come from, so the whole life of a state is in one block.

The practical difference is that a reducer function grows as a switch statement or a chain of whens, and reviewing it means reading it top to bottom. A spec is grouped by state, so reviewing it means reading the life of each state, which is usually how you think about a screen. The cost is the DSL and the extra concept.

The second comparison is with the testing side, and this is where the library's design pays off. Because the machine is a plain object with a state flow and a dispatch method, a test can construct it, dispatch an action and assert on the resulting state, without a UI and without a coroutine test scope for the surrounding view model. There is no mock to write and no view model to instantiate, which is the usual complaint about testing MVI code.

The third comparison is with a library that does the same thing differently, and here the material is thin. The README mentions the best practices documentation and the sample directory with an android/, an ios/ and a shared_code/ layout, which tells you the multiplatform story is exercised in the sample rather than only in tests.

The fourth point is about scope. FlowRedux is a state machine library, not a UI framework, not an architecture and not a DI container. It sits under whatever you use for the view layer, and the compose artifact is a convenience bridge rather than a dependency on Compose.

## Licence, project state and what to check before you adopt it

The licence field on the repository records Apache-2.0, with a LICENSE file at the root, and a permissive licence with a patent grant is a comfortable fit for a library that most adopters will link into a closed-source application.

The project is not archived and the last push was on 2026-10-22, so it is being actively worked on, and the release history shows 2.1.1 from 2026-06-24 and 2.1.0 from the same day, after 2.0.1 from 2026-01-12.

The repository layout reflects a multiplatform Gradle project with an old and a new structure side by side. There is a flowredux/ directory for the core module, a flowredux-extensions/ directory, a sample/ directory with android, ios and shared_code subdirectories, a docs/ directory, an mkdocs.yml for the documentation site, a scripts/ directory, a build.gradle.kts with the Gradle Kotlin DSL, a settings.gradle.kts and a gradlew wrapper.

There is also a RELEASING.md and a CHANGELOG.md at the root.

Four things to check before you put it in an application.

The best practices documentation at freeletics.github.io/FlowRedux, which the README points to for full documentation and best practices, and which is where the intended shape of a spec block is described rather than inferred from the example.

The changelog, because the 2.x line has moved quickly and the API between 2.0 and 2.1 is not guaranteed unchanged.

The sample, since it covers android, ios and shared_code, and the shared code directory is where a multiplatform consumer's actual usage pattern lives.

And the JavaScript question, if you have a web target, because the README is explicit that support is planned but not available yet.

## Conclusion

FlowRedux suits an Android or multiplatform team that has outgrown a plain ViewModel and wants the state transitions of an MVI architecture to be declared in one readable place while keeping the coroutines and Flow that Kotlin already provides. It is the wrong tool for a project that does not want a state machine abstraction, since a reducer plus a sealed class of events is a smaller dependency, and the wrong choice if you need JavaScript today, because the README says that support is planned but not available yet. Before adopting it, read the best practices in the documentation site, since the library is opinionated about how the spec block should be structured, and check the changelog, because the 2.x line has moved quickly.

## FAQ

### What is FlowRedux?

It is a Kotlin Multiplatform library for building asynchronous state machines, with a DSL layered over the Flow type from Kotlin coroutines. States, actions and transitions are declared in a spec block grouped by state, and the machine exposes a state flow that consumers collect and dispatch actions into.

### How do you add and use a FlowRedux dependency?

For a JVM or Android project add the flowredux-jvm artifact and the compose artifact. For a multiplatform project add the single flowredux artifact. Snapshots published from the main branch need the snapshot repository added to your Gradle repositories and a -SNAPSHOT version suffix.

### Which platforms does FlowRedux support?

JVM and Android, iOS, watchOS, tvOS, macOS, Linux and Windows. The README states that support for JavaScript is planned but not available yet.

### What is the difference between state.override and state.mutate in FlowRedux?

An override replaces the state, which is a transition to a different state and is used inside onEnter and action handlers. A mutate changes the current state in place using data class copy semantics, so the state type stays the same and only its data changes.

### How do I use FlowRedux with an Android ViewModel?

Inject the state machine into the ViewModel, collect its state into a MutableLiveData inside a viewModelScope launch, and dispatch actions from a viewModelScope launch as well. The scope is cancelled automatically when the ViewModel lifecycle reaches destroyed.

### What licence is FlowRedux released under?

Apache-2.0, with the terms in the LICENSE file at the root of the repository.

## Sources

- [freeletics/FlowRedux on GitHub](https://github.com/freeletics/FlowRedux)
- [License: Apache-2.0](https://github.com/freeletics/FlowRedux/blob/main/LICENSE)
- [Project website](https://freeletics.github.io/FlowRedux/)
- [README](https://github.com/freeletics/FlowRedux/blob/main/README.md)
- [Releases](https://github.com/freeletics/FlowRedux/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/freeletics-flowredux
