FlowMVI is a Kotlin Multiplatform architectural framework based on coroutines. It enables you to extend your business logic with reusable plugins, handle errors, achieve thread-safety, and more. It takes about 10 minutes to get started.
Version catalogs
[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" }
# 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
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")
// 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")
}
Usually architecture frameworks mean boilerplate 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 automate processes and reuse any business logic you desire
- Create automatic analytics handlers, websocket connections, error handling mechanisms, or anything else once and reuse them throughout your whole project automatically
- Automatically recover from any errors and report them to analytics.
- Build fully async, reactive and parallel apps - with no manual thread synchronization required!
- Create multiplatform business logic components with pluggable UI
- Automatic multiplatform system lifecycle handling
- Out of the box debugging, logging, caching and long-running tasks support
- Debounce, retry, batch, throttle, conflate, monitor, modify any operations automatically
- Compress, persist, and restore state automatically on any platform
- No base classes, complicated interfaces, or factories of factories - logic is declarative and built with a DSL
- Build Restartable, reusable business logic components with no external dependencies or dedicated lifecycles
- Create compile-time safe state machines with a readable DSL. Forget about casts, inconsistent states, and
null
s - First class Compose Multiplatform support optimized for performance and ease of use
- Use both MVVM+ (functional) or MVI (model-driven) style of programming
- Share, distribute, disable, manage side-effects based on your team's needs
- Dedicated IDE Plugin for debugging and codegen and app for Windows, Linux, MacOS
- Integration with popular libraries, such as Decompose (Essenty)
- The core library has no dependencies except kotlin coroutines.
- Core library is fully covered by hundreds of tests
- Minimal performance overhead, equal to using a simple Channel, with regular benchmarking
- Collect, monitor and report performance metrics automatically (upcoming).
- Test any business logic using clean, declarative DSL.
- Learn more by exploring the sample app in your browser
All you have to do is:
sealed interface State : MVIState {
data object Loading : State
data class Error(val e: Exception) : State
data class Content(val counter: Int = 0) : State
}
sealed interface Intent : MVIIntent {
data object ClickedCounter : Intent
}
sealed interface Action : MVIAction {
data class ShowMessage(val message: String) : Action
}
val counterStore = store(initial = State.Loading, scope = coroutineScope) {
// install plugins you need
install(analyticsPlugin)
// recover from errors
recover { e: Exception ->
updateState { State.Error(e) }
null
}
// load data
init {
updateState {
State.Content(counter = repository.loadCounter())
}
}
// respond to events
reduce { intent: Intent ->
when (intent) {
is ClickedCounter -> updateState<State.Content, _> {
action(ShowMessage("Incremented!"))
copy(counter = counter + 1)
}
}
}
}
store.intent(ClickedCounter)
With FlowMVI, complexity does not grow no matter how many features you add. Adding a new feature is as simple as calling a function.
Advanced configuration example
class CounterContainer(
private val repo: CounterRepository, // inject dependencies
) {
val store = store<CounterState, CounterIntent, CounterAction>(initial = Loading) {
configure {
// use various side-effect strategies
actionShareBehavior = Distribute()
// checks and verifies your business logic for you
debuggable = true
// make the store fully async, parallel and thread-safe
parallelIntents = true
coroutineContext = Dispatchers.Default
stateStrategy = Atomic()
}
// out of the box logging
enableLogging()
// debug using the IDE plugin
enableRemoteDebugging()
// undo / redo any operation
val undoRedo = undoRedo()
// manage long-running jobs
val jobManager = manageJobs<CounterJob>()
// save and restore the state automatically
serializeState(
path = repo.cacheFile("counter"),
serializer = DisplayingCounter.serializer(),
)
// perform long-running tasks on startup
init {
repo.startTimer()
}
// save resources when there are no subscribers
whileSubscribed {
repo.timer.collect {
updateState<DisplayingCounter, _> {
copy(timer = timer)
}
}
}
// lazily evaluate and cache values, even when the method is suspending.
val pagingData by cache {
repo.getPagedDataSuspending()
}
// testable reducer as a function
reduce { intent: CounterIntent ->
when (intent) {
// typed state update prevents races and allows using sealed class hierarchies for LCE
is ClickedCounter -> updateState<DisplayingCounter, _> {
copy(counter = counter + 1)
}
}
}
// cleanup resources
deinit {
repo.stopTimer()
}
// and 30+ more options to choose from...
}
}
Powerful DSL allows you to hook into various events and amend any part of your logic:
fun analyticsPlugin(analytics: Analytics) = plugin<MVIState, MVIIntent, MVIAction> {
onStart {
analytics.logScreenView(config.name) // name of the screen
}
onIntent { intent ->
analytics.logUserAction(intent.name)
}
onException { e ->
analytics.logError(e)
}
onSubscribe {
analytics.logEngagementStart()
}
onUnsubscribe {
analytics.logEngagementEnd()
}
onStop {
analytics.logScreenLeave()
}
}
Never write analytics, debugging, or state persistence code again.
Using FlowMVI with Compose is a matter of one line of code:
@Composable
fun CounterScreen() {
val store = counterStore
// subscribe to store based on system lifecycle - on any platform
val state by store.subscribe { action ->
when (action) {
is ShowMessage -> /* ... */
}
}
when (state) {
is DisplayingCounter -> {
Button(onClick = { store.intent(ClickedCounter) }) {
Text("Counter: ${state.counter}")
}
}
}
}
Enjoy testable UI and free @Preview
s.
No more subclassing ViewModel
. Use generic StoreViewModel
instead and make your business logic multiplatform.
val module = module { // Koin example
factoryOf(::CounterContainer)
viewModel(qualifier<CounterContainer>()) { StoreViewModel(get<CounterContainer>()) }
}
class ScreenFragment : Fragment() {
private val vm by viewModel(qualifier<CounterContainer>())
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribe(vm, ::consume, ::render)
}
private fun render(state: CounterState) {
// update your views
}
private fun consume(action: CounterAction) {
// handle actions
}
}
Finally stop writing UI tests and replace them with unit tests:
store.subscribeAndTest {
// turbine + kotest example
ClickedCounter resultsIn {
states.test {
awaitItem() shouldBe State(counter = 1)
}
actions.test {
awaitItem() shouldBe ShowMessage
}
}
}
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)
}
IDE plugin generates code and lets you debug and control your app remotely:
Debugger.mp4
Begin by reading the Quickstart Guide.
Copyright 2022-2024 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
http://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.