VIE
Model-View-Intent for Compose Multiplatform: one state flow down, intents up, signals sideways.
VIE gives a screen three types and a contract between them.
A Model is the state the view renders.
An Intent is a UI event: most often something the user did, but not always.
A Signal is a one-off the view reacts to but does not render: a navigation, a snackbar, a toast.
You declare the three in an interface, a VIEViewModel implements it, and a VIEView renders it.
The view only ever sees the contract, never the ViewModel.
The name is the French translation of MVI: Vue Intention État.
interface CounterVIE : VIE<Model, Intent, Signal> {
data class Model(val count: Int, val saving: Boolean = false) : VIE.Model
sealed interface Intent : VIE.Intent {
data object Increment : Intent
data object Save : Intent
}
sealed interface Signal : VIE.Signal {
data object Saved : Signal, VIE.Signal.Event (1)
data class Error(val message: String) : Signal, VIE.Signal.Effect (2)
}
}
| 1 | An Event is broadcast to every view that listens. |
| 2 | An Effect is handled once, by a single view. |
class CounterVM(
private val store: CounterStore,
) : CounterVIE, VIEViewModel<Model, Intent, Signal>(
firstModel = Model(count = 0),
) {
override suspend fun process(intent: Intent) {
when (intent) {
is Intent.Increment -> update { copy(count = count + 1) }
is Intent.Save -> {
update { copy(saving = true) }
store.save(model.count)
.onSuccess { signal(Signal.Saved) }
.onFailure { signal(Signal.Error(it.message ?: "Save failed")) }
update { copy(saving = false) }
}
}
}
}
@Composable
fun Counter(store: CounterStore, snackbar: SnackbarHostState) {
VIEView(newVM = { CounterVM(store) }) { model -> (1)
SignalEffect { signal ->
when (signal) {
is Signal.Saved -> snackbar.showSnackbar("Saved")
is Signal.Error -> snackbar.showSnackbar(signal.message)
}
}
Column {
Text("Count: ${model.count}")
Button(onClick = { post(Intent.Increment) }) { Text("+") }
Button(
onClick = { post(Intent.Save) },
enabled = !model.saving,
) { Text("Save") }
}
}
}
| 1 | Inside the block, this is a VIEViewScope: it can post intents and listen to signals, nothing more. |
No navigation framework, no event bus, no manual collectAsState.
An interface, a ViewModel, and a composable that is handed the model and a way to talk back.
Why VIE
- The contract is an interface
-
CounterVIEnames the three types once. The ViewModel is one implementation of that interface, and the view depends on the interface, not onCounterVM. Rendering the view in a preview or a test is a different receiver, nothing more. - The view never holds the ViewModel
-
Inside
VIEView, the view sees aVIEViewScopewith exactly two members:post(intent)andsignalFlow. It cannot readmodelFlow, callupdate, or reachviewModelScope. - Events and Effects are different delivery channels
-
An Event is broadcast to every view that currently listens, and dropped if none does. An Effect reaches exactly one view, so a "navigate back" fires once even when two views observe the same ViewModel.
- Model updates are serialized
-
update { }hands its block to a single consumer and suspends until it is applied. Two coroutines incrementing the count at the same time cannot lose an increment. - Built on
ViewModel, not beside it -
VIEViewModelextendsandroidx.lifecycle.ViewModel, soviewModelScope,viewModel { }and Navigation3 entry scoping work unchanged. VIE adds the MVI shape, it does not replace the lifecycle. - Middlewares wrap the flows
-
A
VIEMiddlewareintercepts the model, intent and signal flows as plainFlowtransformations: logging, filtering, inspecting. Neither the ViewModel nor the view knows it is there.
How to read this documentation
-
Getting started gets a first screen running in a few minutes.
-
The Core section explains VIE from the ground up: the contract, the ViewModel, the view, then middlewares.
-
The Testing section shows how to preview a view and test both sides of a screen without running the other.
-
The Guides section answers questions the core pages leave open, starting with Navigating between screens, Providing dependencies, Forms and text input and ViewModel lifecycles.
-
AI agent skill installs the agent skill that teaches VIE to your coding assistant.