Skip to content
/ fluxo Public

Fluxo: Simple yet super powerful state-management framework for Kotlin Multiplatform and Android (MVI/MVVM+)

License

Notifications You must be signed in to change notification settings

fluxo-kt/fluxo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Fluxo

Stability: Alpha Snapshot Version Kotlin Version Build codecov License

Kotlin Multiplatform JVM badge badge badge badge badge badge badge badge


Fluxo [ˈfluksu] is a simple yet super powerful state management library for Kotlin Multiplatform.

Approach is best known as BLoC1, MVI2, MVVM+3, Redux4, TEA/Elm/MVU5, SAM6, or even State Machine/FSM7.
Often used in the UI or presentation layers of the architecture.
Suitable and proven useful for any architectural layer of the app for any platform.

If you need predictable unidirectional data flow (UDF) or deterministic control over your state changes, Fluxo will get you covered!

⚠ Work-In-Progress, first release is coming. API isn’t stable yet!

TLDR: Use SNAPSHOT artefact in Gradle (in a safe and reproducible way)

Latest snapshot
Select a snapshot for the preferred commit using a scheme: 0.1-<SHORT_COMMIT_SHA>-SNAPSHOT.
For example: 0.1-140e32d-SNAPSHOT

implementation("io.github.fluxo-kt:fluxo-core:0.1-<SHORT_COMMIT_SHA>-SNAPSHOT")
// For common data states
implementation("io.github.fluxo-kt:fluxo-data:0.1-<SHORT_COMMIT_SHA>-SNAPSHOT")
// in `settings.gradle.kts` of the project
repositories {
  maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
Toml
[versions]
fluxo = "0.1-<SHORT_COMMIT_SHA>-SNAPSHOT"

[libraries]
fluxo-core = { module = "io.github.fluxo-kt:fluxo-core", version.ref = "fluxo" }
fluxo-data = { module = "io.github.fluxo-kt:fluxo-data", version.ref = "fluxo" }

Code examples

MVVM+ container with lambda intents.

import kotlinx.coroutines.*
import kt.fluxo.core.*
import kotlin.coroutines.coroutineContext

suspend fun main() {
  // State here is an Int value. One-liner creation! Type inference is automatic!
  val container = container(initialState = 0)
  repeat(100) {
    container.intent {
      // `updateState {}` used to access the previous value safely.
      // Use `value = ...` if previous value isn’t needed.
      updateState { prev -> prev + it }

      // suspendable lambda intent, it can start long-running side jobs and send side effects!
    }
  }

  // Listen to reactive state updates somewhere. Container is a `StateFlow`!
  // Supports cooperative cancellation on container close.
  CoroutineScope(coroutineContext).launch {
    container.collect(::println)
  }

  // You can wait for the full completion,
  // but it’s optional, `container.close()` is also available.
  container.closeAndWait()
}
Strict Redux/MVI store with pure non-suspendable reducer.
import kt.fluxo.core.*

suspend fun main() {
  // Type inference is automatic if the reducer argument type is specified!
  val store = store<Int /* Intent */, Int /* State */>(
    initialState = 0,
    // pure non-suspendable reducer
    // `State.(Intent) -> State`
    reducer = { this + it }
  )
  repeat(100) {
    // emit is suspendable
    // use `send` methods for intents from non-suspend contexts.
    store.emit(it)
  }
}
Strict MVI intents with powerful MVVM+ DSL in suspendable handler.
import kt.fluxo.core.*

suspend fun main() {
  // Type inference is automatic if the handler argument type is specified!
  val store = store(
    initialState = 0,
    // suspendable handler, it can start long-running side jobs and send side effects!
    handler = { intent: Int ->
      updateState { prev -> prev + intent }
    }
  )
  // emit is suspendable
  // use `send` methods for intents from non-suspend contexts.
  repeat(100) { store.emit(it) }
}

Overview

Fluxo was started as a test for the hypothesis: it should be possible to combine all the strong sides of strict Redux/MVI4 with flexibility, ease of readability, and maintainability of the MVVM+3.

The experiment paid off! It is possible to combine MVVM+ with great performance, high-quality time-travel, logging, auto analysis of the transition graph and much more. A long list of features is implemented gradually in this library (see the Roadmap for details).

Basic usage is elementary, yet you can take advantage of fine-tuning and super powerful features when you need them.

  • Kotlin coroutine-based state container.
  • One-liner creation, simple usage, type-safe, no-boilerplate!
  • Use Fluxo for the UI, business, or data tasks with the same ease.
  • Native integration with coroutines:
    • Each Fluxo Store is a StateFlow with states and a FlowCollector for intents. You can easily combine stores with each other and with any other flows, flow operators, and collectors.
    • Also, Fluxo Store is a CoroutineScope itself, so you can integrate it with any existing coroutine workflow and treat just as a usual coroutine scope.
  • Multiplatform, supports all KMP/KMM7 targets (Android, iOS, JVM, JS, Linux, Windows/MinGW, macOS, watchOS, tvOS).
  • Different usage styles:
    • Strict Redux/MVI4 (the highest correctness guarantees, but may be subjectively less readable and intuitive)
    • Flexible MVVM+3 (intuitively readable, may be easier to maintain, has support for every feature and more :)
      • Fluxo provides unique, pretty logging and debugging capabilities for MVVM+3 lambda intents. You can use them with almost the same convenience as discrete MVI intents. And it opens up the possibility of time travel and many other features on the roadmap.
    • Redux-style discrete intents with MVVM+ style reduction DSL (hybrid way)
    • More is coming...
  • Side jobs for long-running tasks (MVVM+ DSL).
    • For example, start a long-going task safely on intent. Jobs with the same name auto cancel earlier started ones.
    • Run something only when listeners are attached using repeatOnSubscription.
    • Recover from any error within the side job with an onError handler.
  • Bootstrap, kind of optional initialization side job that starts on the Store launch.
  • Side effect support (sometimes called news or one-off event).
    • Note that using side effects is generally considered as antipattern!8910.
      But it can still be useful sometimes, especially when migrating an old codebase.
      However, if you find yourself in one of these situations, reconsider what this one-time event actually means for your app. Handle events immediately and reduce them to state. State is a better representation of the given point in time, and it gives you more delivery and processing guarantees. State is usually easier to test, and it integrates consistently with the rest of your app.
    • Fluxo has four strategies to fully control how side effects are shared from the store (RECEIVE, CONSUME, SHARE, DISABLE).
    • Side effects are cached while the subscriber (e.g., view) isn’t attached.
    • Side effects can have consumption guarantees with GuaranteedEffect (effect handled and exactly once)1112.
  • Lifecycle-awareness with full control based on coroutine scopes.
  • Subscription lifecycle with convenience API (repeatOnSubscription).
    • Do anything when store subscriber connects or disconnects.
    • Listen to the number of subscribers with subscriptionCount StateFlow.
  • Forceful customization:
    • Pluggable intent strategies:
      • First In, First Out (Fifo). Default, predictable, and intuitive, ordered processing with good performance.
      • Last In, First Out (Lifo). Can improve responsiveness, e.g. UI events processing, but may lose some intents!
      • Parallel. No processing order guarantees, can provide better performance and responsiveness compared to Fifo.
      • Direct. No pipeline. Immediately executes every intent until the first suspension point in the current thread.
      • ChannelLifo. Special Channel-based Lifo implementation that provides extra customization compared to Lifo.
      • Create your own!
    • Eager or lazy initialization of the store (lazy by default).
    • Global default settings for easier setup of state stores swarm.
      • Change settings once and for all at one place (FluxoSettings.DEFAULT).
      • Provide a prepared settings object for the group of your stores.
      • Or configure each store individually.
  • Common data states in a fluxo-data module (Success, Failure, Loading, Cached, Empty, Not Loaded).
  • Error handling and exception behavior control.
    • On the level of StoreFactory (for many Stores).
    • For each Store.
    • For each launched sideJob.
  • Leak-free transfer, delivery guarantees1314 for intents and side effects.
  • Complete interception for any possible event (like in OkHttp, etc.)
  • Strictly not recommended, but Closeable resources are experimentally supported as a state and side effects.
    • The previous state is closed on change.
    • Side effects are closed when not delivered.
    • However, no clear guarantees!
  • Intentionally unopinionated, extensible API: you can follow guides or use it as you want.
  • Well tested.
  • Great performance.
  • Reactive streams compatibility through coroutine wrappers:
    • RxJava 2.x, RxJava 3.x
    • JDK 9 Flow, Reactive Streams
    • Project Reactor
  • LiveData compatibility with AndroidX.

Known IDE issues

  • ⚠ IDE actions «Go to declaration» and «Quick Documentation» don’t work for container builders due to KT-58512.

Benchmarks and research

Compares the performance of different MVI state-management libraries.
Deep feature comparison research is in-progress.

Single-thread simple incrementing intents (16 tests, 2023-09-30), updates on CI.
Each operation creates a state store, sends 5000 intents with reduction, and checks state updates!

Benchmark Score Units Percent
reduxkotlin__mvi_reducer 10.077 ± 0.099 ops/ms 0.0%
mvicore__mvi_reducer 6.071 ± 0.066 ops/ms -39.8%
visualfsm__sm_reducer 5.536 ± 0.044 ops/ms -45.1%
fluxo__mvi_reducer (*) 5.268 ± 0.047 ops/ms -47.7%
tindersm__sm_reducer 2.383 ± 0.040 ops/ms -76.4%
mvikotlin__mvi_reducer 2.144 ± 0.030 ops/ms -78.7%
reduktor__mvi_reducer 1.982 ± 0.024 ops/ms -80.3%
fluxo__mvi_handler (*) 1.460 ± 0.088 ops/ms -85.5%
genakureduce__mvi_handler 1.441 ± 0.048 ops/ms -85.7%
fluxo__mvvmp_intent (*) 1.407 ± 0.113 ops/ms -86.0%
mobiuskt__sm_reducer 1.037 ± 0.081 ops/ms -89.7%
elmslie__elm_reducer 0.669 ± 0.016 ops/ms -93.4%
orbit__mvvmp_intent 0.640 ± 0.014 ops/ms -93.6%
ballast__mvi_handler 0.467 ± 0.014 ops/ms -95.4%
flowmvi__mvi_handler 0.329 ± 0.008 ops/ms -96.7%
flowredux__mvi_handler 0.138 ± 0.002 ops/ms -98.6%

Roadmap

  • Support new Kotlin AutoCloseable interface (since Kotlin 1.8.20, Android API level 19)
  • sideJob helpers for logic called on a specific state or side effect (see Kotlin-Bloc).
  • SAM: State-Action-Model, composable
    • functions as first-class citizens
  • Store to Store connection
  • FSM: Strict finite-state machine style with edges declaration
  • SideJobs registry/management API
  • Partial state change with effect
  • Debug checks
  • (Optional) Java-friendly API
  • Compose integration tests, examples and docs
  • ViewModel integration tests, examples and docs
  • SavedState (android state preservation) integration tests, examples and docs
  • Essenty integration tests, examples and docs
  • Compose Desktop (JetBrains) integration tests, examples and docs
  • Rx* libraries integration tests, examples and docs
  • LiveData integration tests, examples and docs
  • Arrow integration tests, examples and docs
  • Time-travel (MviKotlin, Ballast, Flipper integration)
  • Logging module
  • Unit test library
    • Espresso idling resource support
  • DI support tests, examples and docs
  • JS/TS usage examples and NPM publication
  • State graph tools
    • Get unreachable states, build an Edge List, build states Adjacency Map.
    • App containers aggregation for graph tools
  • Analytics/Crashlytics integration
  • Orbit, MVI Kotlin, etc. migration examples and tests for migration helpers.
  • Documentation and examples
  • (Optional) Undo/Redo
  • (Optional) Stores synchronization

Heavily inspired by

Versioning

Fluxo uses SemVer for versioning. For the versions available, see the tags on this repository.

Code quality and more

Lines-of-Code Hits-of-Code, number of lines changed over time
CodeFactor CodeBeat Codacy Sonatype Lift
CodeClimate

License

License

This project is licensed under the Apache License, Version 2.0 — see the license file for details.

Footnotes

  1. BLoC stands for Business Logic Components, architectural pattern [1, 2]

  2. MVI: Model-View-Intent architectural pattern.

  3. MVVM+, orbit-way: updated Model-View-ViewModel pattern, aka Redux/MVI with contextual reduction. 2 3 4

  4. Redux: Pattern for predictable managing and updating app state, and a famous library. 2 3

  5. TEA: The Elm Architecture. MVU (Model-View-Update) is a core of it.

  6. SAM: State-Action-Model architectural pattern.

  7. FSM: Finite-State Machine. 2

  8. ViewModel: One-off event antipatterns (2022, by Manuel Vivo from Google)

  9. Google Guide to app architecture, UI events > Other use cases > Note (Apr 2023)

  10. How To Handle ViewModel One-Time Events In Jetpack Compose, One-Time-Event Anti-Pattern] (2022, by Yanneck Reiß)

  11. [Proposal] Primitive or Channel that guarantees the delivery and processing of items (Kotlin/kotlinx.coroutines#2886)

  12. SingleLiveEvent case with an Event wrapper.

  13. Leak-free closeable resources transfer via Channel (Kotlin/kotlinx.coroutines#1936)

  14. Unfortunately events may be dropped from kotlinx.coroutines Channel