
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 workflow/runtime layer for Kotlin Multiplatform features.
Kmposable lets you structure feature logic as pure, testable Nodes (state + events + outputs)
and run them headlessly with NavFlow.
For Compose KMP apps, the recommended architecture is Navigation 3 KMP for the app shell and
kmposable for inner feature workflows.
Build feature workflows without UI.
Plug them into Navigation 3 KMP or another shell later.
Test the business logic 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 feature workflow, drives node transitions, exposes state.
Node (state, events, outputs)
|
| outputs
v
Navigator <---- NavFlow ----> UI Renderer
That’s the core of Kmposable.
For 0.3.x, the primary Compose story is:
Use library-navigation3 to host kmposable flows inside Navigation 3 destinations.
Navigation 3 KMP + kmposable
Recommended for new Compose KMP apps.Non-Nav3 Compose + kmposable
Supported fallback for teams that are not on Navigation 3 yet or intentionally want a smaller setup.Detailed docs now live at https://mobiletoly.github.io/kmposable:
0.3.x app-shell split.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.updateTopNode<CounterNode> { onEvent(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 a Navigation 3 KMP shell and inner kmposable flow.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 workflow/runtime layer for Kotlin Multiplatform features.
Kmposable lets you structure feature logic as pure, testable Nodes (state + events + outputs)
and run them headlessly with NavFlow.
For Compose KMP apps, the recommended architecture is Navigation 3 KMP for the app shell and
kmposable for inner feature workflows.
Build feature workflows without UI.
Plug them into Navigation 3 KMP or another shell later.
Test the business logic 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 feature workflow, drives node transitions, exposes state.
Node (state, events, outputs)
|
| outputs
v
Navigator <---- NavFlow ----> UI Renderer
That’s the core of Kmposable.
For 0.3.x, the primary Compose story is:
Use library-navigation3 to host kmposable flows inside Navigation 3 destinations.
Navigation 3 KMP + kmposable
Recommended for new Compose KMP apps.Non-Nav3 Compose + kmposable
Supported fallback for teams that are not on Navigation 3 yet or intentionally want a smaller setup.Detailed docs now live at https://mobiletoly.github.io/kmposable:
0.3.x app-shell split.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.updateTopNode<CounterNode> { onEvent(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 a Navigation 3 KMP shell and inner kmposable flow.sample-app-flowscript — Same UI but orchestrated via a NavFlow script.Both samples include READMEs with run/test instructions.
dev.goquick.kmposable:*
spec_docs/