
Tiny, Compose-first MVI toolkit enabling unidirectional state management via pure reducers, suspend side-effects and an interactor that folds events and upstream flows into StateFlow; easy to embed.
A tiny, Compose-first MVI toolkit designed to be easy to learn, portable across KMP targets, and simple to embed in existing apps. It provides just enough structure to build unidirectional data flows without locking you into a large framework.
// build.gradle.kts
dependencies {
implementation("tv.dpal:flowvi:<version>")
// optional
implementation("tv.dpal:flowvi-compose:<version>")
}Define your State and Event, create reducers and side-effects, and wire them up in your Composable using rememberInteractor from mvi:compose.
data class CounterState(val value: Int = 0)
sealed interface CounterEvent {
data object Increment : CounterEvent
data object Decrement : CounterEvent
}
val counterReducer = Reducer<CounterState, CounterEvent> { state, event ->
when (event) {
CounterEvent.Increment -> state.copy(value = state.value + 1)
CounterEvent.Decrement -> state.copy(value = state.value - 1)
}
}
val logSideEffect: SideEffect<CounterState, CounterEvent> = { state, event ->
// fire-and-forget logging, analytics, etc.
}
@Composable
fun CounterScreen() {
val interactor = rememberInteractor(
initialState = CounterState(),
reducers = listOf(counterReducer),
sideEffects = listOf(logSideEffect),
source = { initial -> flow { emit(initial) } }, // no upstream flow in this example
)
val state by interactor.state.collectAsState()
// UI... dispatch events with interactor(CounterEvent.Increment)
}@Serializable to enable automatic persistence via rememberSaveable in :mvi:compose.state: StateFlow<State> and a function-call operator fun invoke(event) to dispatch.Flow<State> for your upstream data (e.g., repositories). The interactor will fold events on top of the latest upstream emission.rememberInteractor(
initialState = MyState(),
reducers = reducers,
sideEffects = sideEffects,
source = { initial -> repo.stateFlow() },
)State restoring
rememberSaveable is used under the hood with kotlinx.serialization. If your State is not serializable, provide your own saver or disable persistence by wrapping rememberInteractor.
Threading
Dispatchers.Default.Lifecycle
The returned Interactor is hosted by the provided CoroutineScope (defaults to a Compose scope). On Android, it behaves like a view-model–scoped state holder.
A tiny, Compose-first MVI toolkit designed to be easy to learn, portable across KMP targets, and simple to embed in existing apps. It provides just enough structure to build unidirectional data flows without locking you into a large framework.
// build.gradle.kts
dependencies {
implementation("tv.dpal:flowvi:<version>")
// optional
implementation("tv.dpal:flowvi-compose:<version>")
}Define your State and Event, create reducers and side-effects, and wire them up in your Composable using rememberInteractor from mvi:compose.
data class CounterState(val value: Int = 0)
sealed interface CounterEvent {
data object Increment : CounterEvent
data object Decrement : CounterEvent
}
val counterReducer = Reducer<CounterState, CounterEvent> { state, event ->
when (event) {
CounterEvent.Increment -> state.copy(value = state.value + 1)
CounterEvent.Decrement -> state.copy(value = state.value - 1)
}
}
val logSideEffect: SideEffect<CounterState, CounterEvent> = { state, event ->
// fire-and-forget logging, analytics, etc.
}
@Composable
fun CounterScreen() {
val interactor = rememberInteractor(
initialState = CounterState(),
reducers = listOf(counterReducer),
sideEffects = listOf(logSideEffect),
source = { initial -> flow { emit(initial) } }, // no upstream flow in this example
)
val state by interactor.state.collectAsState()
// UI... dispatch events with interactor(CounterEvent.Increment)
}@Serializable to enable automatic persistence via rememberSaveable in :mvi:compose.state: StateFlow<State> and a function-call operator fun invoke(event) to dispatch.Flow<State> for your upstream data (e.g., repositories). The interactor will fold events on top of the latest upstream emission.rememberInteractor(
initialState = MyState(),
reducers = reducers,
sideEffects = sideEffects,
source = { initial -> repo.stateFlow() },
)State restoring
rememberSaveable is used under the hood with kotlinx.serialization. If your State is not serializable, provide your own saver or disable persistence by wrapping rememberInteractor.
Threading
Dispatchers.Default.Lifecycle
The returned Interactor is hosted by the provided CoroutineScope (defaults to a Compose scope). On Android, it behaves like a view-model–scoped state holder.