Guides
Start Here
- Hello Kmposable – the smallest flow: install, node, NavFlow, Compose host, headless test.
- Core Guides (below) – deep dives into Compose integration, scripts, testing, and advanced patterns.
Choose A Compose Path
- Recommended: Navigation 3 KMP
- Supported fallback: Non-Nav3 Compose Hosting
- Quick comparison: Nav3 vs Non-Nav3
1. Getting Started
Install
dependencies {
implementation("dev.goquick.kmposable:core:<version>")
implementation("dev.goquick.kmposable:compose:<version>")
implementation("dev.goquick.kmposable:navigation3:<version>") // when using Navigation 3 KMP
testImplementation("dev.goquick.kmposable:test:<version>")
}
Replace <version> with the latest release from Maven Central. core is mandatory, compose is
needed only if you render with Jetpack Compose, navigation3 is the preferred Compose KMP
integration layer, and test pulls in FlowTestScenario utilities.
Smallest NavFlow
A NavFlow is a stack of Nodes. Each node is a tiny state machine:
STATE– immutable UI state exposed viaStateFlowEVENT– inputs from UI (onEvent(event))OUTPUT– signals sent upward (navigation/results). UseNothingif the node never emits outputs.
Flow of data: UI → EVENT → Node updates STATE → (optional) OUTPUT → NavFlow reacts (push/pop/replace).
StatefulNode<STATE, EVENT, OUTPUT> generics map to your types: STATE is the immutable state
(StateFlow<STATE>), EVENT is what the node accepts via onEvent (usually UI/user actions), and
OUTPUT is what the node emits upward to a parent flow/runtime (navigation/results). Use Nothing
when the node does not emit outputs.
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(appScope = scope, rootNode = CounterNode(scope)).apply { start() }
navFlow.updateTopNode<CounterNode> { onEvent(CounterEvent.Increment) }
println(navFlow.navState.value.top.state.value) // 1
How this reaches UI:
- UI observes
node.state(e.g.,collectAsState()in Compose) and renders it. - UI sends user actions back as
EVENTs vianode.onEvent(...)ornavFlow.updateTopNode<MyNode> { ... }when it only has a NavFlow reference. - If a node emits
OUTPUT, NavFlow (or a parent) uses it to navigate or surface results.
2. Compose Integration
Remembering a NavFlow
@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, onIncrement = { node.onEvent(CounterEvent.Increment) })
}
}
}
NavFlowHost(navFlow = navFlow, renderer = renderer)
}
Navigation 3 KMP rules
- Navigation 3 KMP owns app routes, outer back stack, and destination save/restore.
- Inside each destination, create a kmposable feature runtime and render it via
NavFlowHost. - Map feature outputs to app-owned back stack mutations in the app layer, not inside nodes.
- Use
rememberKmposableNavEntryDecorators()so Navigation 3 owns destination-scoped saveable state andViewModelStores. - Keep node/business logic Navigation 3 free. The adapter layer is where app routing happens.
See Navigation 3 KMP for the current recommended shape.
Non-Nav3 Compose rules
- Use
NavFlowHostas a feature host, not as the preferred future-facing app-shell story. - Keep the outer shell explicit in your app code.
- Treat overlays as feature-local helpers rather than shell infrastructure.
- Plan to migrate app-shell concerns to Navigation 3 KMP if you want the primary
0.3.xarchitecture.
See Non-Nav3 Compose Hosting for the fallback path.
3. Testing (Reactive or Scripted Flows)
FlowTestScenario drives NavFlow headlessly:
val scenario = factory.createTestScenario(this).start()
scenario.launchScript { runContactsFlowScript(repository, this@runTest) }
scenario.awaitTopNodeIs<ContactDetailsNode>()
scenario.assertStackTags("ContactsList", "ContactDetails")
scenario.finish()
Highlights:
start(),updateTopNode<T> { ... },pop()– basic stack control.awaitTopNodeIs<T>(),awaitStackSize,awaitStackTags– synchronise with navigation.awaitOutputOfType<T>(),awaitMappedOutput { … }– wait for outputs.launchScript(onTrace = …)– run the same NavFlow script you ship.
4. Advanced Patterns
- DelegatingNode – derive from it to wrap another node and override only what you need
(e.g., map outputs, hook
onDetach). - Typed stack entries – create custom entries when you need metadata alongside nodes.
- DI/ViewModels – combine
rememberNavFlowwith your DI container; no Android-specific dependencies required. - Tracing – feed
onTraceinto scripts to integrate with analytics or loggers. - Side effects – implement
EffectSourceor extendEffectfulStatefulNodewhen you need a one-off effects stream (analytics, toasts) separate from navigation outputs. In Compose, collect them withCollectEffects(node) { effect -> /* show snackbar / log */ }to keep hosts lean and lifecycle-aware. - Result-only subflows – extend
ResultOnlyNode<STATE, EVENT, RESULT>when a screen should only return a result (OUTPUT = Nothing). Push it withpushAndAwaitResultOnly/launchPushAndAwaitResultOnlyso you don’t have to thread the NavFlow’s OUT through result-only nodes.
For scripts specifically, see NavFlow Scripts and the Cookbook.