Infrastructure
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.
[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" }
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:
null
sAll 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.
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:
https://github.com/user-attachments/assets/05f8efdb-d125-4c4a-9bda-79875f22578f
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.