
UI-agnostic navigation and flow engine modeling screens as pure Nodes (state, events, outputs), enabling headless navigation, reusable flows, clean UI adapters, and full flow testing.
A UI-agnostic navigation + flow engine for Kotlin Multiplatform.
Kmposable lets you structure your app as pure, testable Nodes (state + events + outputs).
Compose UI becomes a thin rendering layer on top.
Build your entire app flow without UI.
Add UI later.
Test everything headlessly.
Modern Compose apps often struggle with:
Kmposable fixes all of this by giving you:
State + events + outputs. No UI code.
Push, pop, replace — all headless.
Use FlowTestScenario to test deep navigation and logic headlessly (e.g., awaitTopNodeIs<DetailsNode>(), awaitStackTags("root", "details")).
Compose just observes state from your nodes.
You only need three concepts:
A screen/feature logic unit:
state + events + outputs.
Manages a stack of nodes.
Runs the flow, drives navigation, exposes state.
Node (state, events, outputs)
|
| outputs
v
Navigator <---- NavFlow ----> UI Renderer
That’s the core of Kmposable.
Detailed docs now live at https://mobiletoly.github.io/kmposable:
spec_docs/.Define a node once, render or test it anywhere:
data class CounterState(val value: Int = 0)
sealed interface CounterEvent {
object Increment : CounterEvent
object Decrement : CounterEvent
}
class CounterNode(parentScope: CoroutineScope) :
StatefulNode<CounterState, CounterEvent, Nothing>(parentScope, CounterState()) {
override fun onEvent(event: CounterEvent) {
when (event) {
CounterEvent.Increment -> updateState { it.copy(value = it.value + 1) }
CounterEvent.Decrement -> updateState { it.copy(value = it.value - 1) }
}
}
}
val navFlow = NavFlow(scope, CounterNode(scope)).apply { start() }
navFlow.sendEvent(CounterEvent.Increment)Hook it up to Compose with a renderer:
@Composable
fun CounterScreen() {
val navFlow = rememberNavFlow { scope -> NavFlow(scope, CounterNode(scope)) }
val renderer = remember {
nodeRenderer<Nothing> {
register<CounterNode> { node ->
val state by node.state.collectAsState()
CounterUi(state.value) { node.onEvent(CounterEvent.Increment) }
}
}
}
NavFlowHost(navFlow = navFlow, renderer = renderer)
}Tests reuse the exact same flow via SimpleNavFlowFactory + FlowTestScenario. Scripts reuse it via
navFlow.runScript { … } (alias for launchNavFlowScript).
See the docs for full walkthroughs.
sample-app-compose — Compose Multiplatform app with NavHost tabs + Kmposable flows.sample-app-flowscript — Same UI but orchestrated via a NavFlow script.Both samples include READMEs with run/test instructions.
dev.goquick.kmposable:*
spec_docs/
A UI-agnostic navigation + flow engine for Kotlin Multiplatform.
Kmposable lets you structure your app as pure, testable Nodes (state + events + outputs).
Compose UI becomes a thin rendering layer on top.
Build your entire app flow without UI.
Add UI later.
Test everything headlessly.
Modern Compose apps often struggle with:
Kmposable fixes all of this by giving you:
State + events + outputs. No UI code.
Push, pop, replace — all headless.
Use FlowTestScenario to test deep navigation and logic headlessly (e.g., awaitTopNodeIs<DetailsNode>(), awaitStackTags("root", "details")).
Compose just observes state from your nodes.
You only need three concepts:
A screen/feature logic unit:
state + events + outputs.
Manages a stack of nodes.
Runs the flow, drives navigation, exposes state.
Node (state, events, outputs)
|
| outputs
v
Navigator <---- NavFlow ----> UI Renderer
That’s the core of Kmposable.
Detailed docs now live at https://mobiletoly.github.io/kmposable:
spec_docs/.Define a node once, render or test it anywhere:
data class CounterState(val value: Int = 0)
sealed interface CounterEvent {
object Increment : CounterEvent
object Decrement : CounterEvent
}
class CounterNode(parentScope: CoroutineScope) :
StatefulNode<CounterState, CounterEvent, Nothing>(parentScope, CounterState()) {
override fun onEvent(event: CounterEvent) {
when (event) {
CounterEvent.Increment -> updateState { it.copy(value = it.value + 1) }
CounterEvent.Decrement -> updateState { it.copy(value = it.value - 1) }
}
}
}
val navFlow = NavFlow(scope, CounterNode(scope)).apply { start() }
navFlow.sendEvent(CounterEvent.Increment)Hook it up to Compose with a renderer:
@Composable
fun CounterScreen() {
val navFlow = rememberNavFlow { scope -> NavFlow(scope, CounterNode(scope)) }
val renderer = remember {
nodeRenderer<Nothing> {
register<CounterNode> { node ->
val state by node.state.collectAsState()
CounterUi(state.value) { node.onEvent(CounterEvent.Increment) }
}
}
}
NavFlowHost(navFlow = navFlow, renderer = renderer)
}Tests reuse the exact same flow via SimpleNavFlowFactory + FlowTestScenario. Scripts reuse it via
navFlow.runScript { … } (alias for launchNavFlowScript).
See the docs for full walkthroughs.
sample-app-compose — Compose Multiplatform app with NavHost tabs + Kmposable flows.sample-app-flowscript — Same UI but orchestrated via a NavFlow script.Both samples include READMEs with run/test instructions.
dev.goquick.kmposable:*
spec_docs/