| name | kotlin-navigation-compose-multiplatform |
| description | Use when designing, implementing, or reviewing navigation in Compose Multiplatform projects — route modeling, back stack ownership, argument passing, NavOptions, conditional flows, deep links, and adaptive navigation UI. |
| license | Apache-2.0 |
| metadata | {"author":"Mariano Miani","version":"1.2.0"} |
Compose Multiplatform Navigation
Use this skill when designing, implementing, or reviewing navigation in a Compose Multiplatform project.
This skill is intentionally strict. Its purpose is to keep route modeling coherent, back stack behavior explicit, argument passing minimal, navigation side effects controlled, and multiplatform routing boundaries clean across Android and shared Compose code.
Navigation library scope: This skill covers Jetpack Navigation 2 and its Compose Multiplatform equivalent — the current stable navigation library. Navigation 3 (Jetpack Navigation 3, a ground-up redesign) was in alpha as of mid-2025 and has a substantially different back-stack model, entry API, and lifecycle integration. If your project uses Navigation 3, verify which guidance applies before proceeding. Navigation 3 status: https://developer.android.com/guide/navigation/navigation3
Primary goals
The navigation design should optimize for:
- clear route ownership
- explicit back stack ownership
- controlled navigation side effects
- minimal and stable argument passing
- predictable back-stack behavior
- conditional flows that preserve user context
- coherent deep-link handling
- separation between shared routing logic and platform entry concerns
- adaptive navigation chrome for different window sizes
- previewable and testable navigation-hosting UI
Do not treat navigation as only “go from screen A to screen B”.
Treat it as app-state movement with user-history consequences.
Navigation library version note
This skill covers Compose Multiplatform Navigation 2 (current stable as of mid-2025), which is the navigation-compose integration used in most KMP projects today.
Jetpack Navigation 3 (a significant redesign with a different back-stack model built around NavDisplay and NavEntry) was in alpha as of mid-2025. If the project is using Navigation 3, some of this guidance — particularly around NavController ownership, popUpTo, and argument passing — applies differently. Verify against current Navigation 3 documentation before applying this skill to a Navigation 3 project.
Navigation library version scope
This skill covers Jetpack Navigation 2 (stable) as used in Compose Multiplatform, including the compose-navigation artifact and the NavHost/NavController model. Navigation 3 (a separate alpha-stage redesign as of mid-2025) uses a fundamentally different back-stack model and is not covered here. If the project uses Navigation 3, verify that the patterns below still apply — some will, some will not. Check the Navigation 3 documentation for current guidance.
Official defaults to prefer
Unless the project has a strong reason not to, prefer:
- a single owner of
NavController or equivalent navigation state
- event-based navigation from composables instead of passing
NavController downward
- typed or otherwise structured routes
- minimal argument payloads, usually identifiers rather than complex objects
- explicit use of back-stack options like
popUpTo, inclusive, saveState, restoreState, and launchSingleTop
- conditional navigation driven by shared state rather than duplicated guards
- deep-link patterns that are non-overlapping and predictable
- shared route interpretation in common code when valid across targets
- browser URL binding only at the platform/web edge
Review dimensions
1. Route modeling
Check whether routes are modeled clearly and intentionally.
Prefer:
- route models that are explicit and typed where possible
- destination identity that is understandable from the code
- route definitions separated from screen implementation details
In Compose Multiplatform navigation, a route identifies a destination and defines the arguments required to navigate there, while staying separate from the UI implementation.
Flag as a concern when:
- routes are brittle string literals scattered through the app
- route parameters are implicit or weakly structured
- screen identity and navigation actions are hard to trace
2. NavController ownership and navigation events
Android explicitly recommends that composables expose navigation events rather than receiving a NavController reference directly.
Check whether:
- the navigation host or app shell owns the
NavController
- lower composables expose callbacks like
onOpenDetails() rather than calling navigate() directly
navigate() is called from callbacks or controlled side effects, not from normal rendering paths
Flag as a concern when:
NavController is passed deep into leaf composables
- leaf composables call
navigate() directly
- navigation can be retriggered on recomposition
3. Back stack ownership
Navigation state should have a clear owner.
Check whether:
- the back stack is owned by one clear navigation state holder
- navigation mutations happen through a small number of controlled paths
- unrelated screens or helpers are not mutating navigation state ad hoc
Flag as a concern when:
- many layers can push and pop destinations freely
- back stack state is effectively global without clear ownership
- destination changes are triggered from rendering code without architectural control
4. Basic navigation policy
Android’s Navigation docs treat navigate() and popBackStack() as core app-history operations.
Check whether:
- forward navigation uses the right destination key or typed route
- back navigation uses
popBackStack() or navigateUp() intentionally
- failed
popBackStack() cases are handled safely instead of leaving the app in an invalid state
Flag as a concern when:
- back behavior is assumed rather than designed
- popping the last destination can leave a blank screen with no recovery path
- history behavior is inconsistent across similar flows
5. NavOptions and stack-shaping rules
Review the explicit use of navigation options.
Important options include:
popUpTo()
inclusive
saveState
restoreState
launchSingleTop
Check whether:
popUpTo() is used to shape history intentionally
inclusive is used only when removing the target destination is correct
saveState / restoreState are used where returning to preserved stacks matters
launchSingleTop is used to avoid duplicate top destinations
Flag as a concern when:
- duplicate destinations accumulate unnecessarily
- history is rewritten accidentally
- stack state should be preserved but is discarded
popUpTo() behavior is hard to reason about
6. Argument-passing discipline
Android recommends passing the minimum necessary information and avoiding complex object transfer through navigation arguments.
Check whether:
- arguments are small and stable
- identifiers are passed instead of whole UI/data models
- destinations load their own data from minimal navigation input
- route serialization stays understandable
Flag as a concern when:
- complex objects are passed through navigation
- route payloads become a substitute for state ownership
- process recreation would be fragile because arguments are oversized or inconsistent
7. Conditional navigation
Conditional navigation should be modeled through shared state and explicit transitions, not scattered checks.
Check whether:
- auth gates, onboarding gates, permission gates, or result-based flows are modeled through shared state
- returning from a conditional flow preserves user context
- prior back-stack entries or saved state are used intentionally where results must be returned
Flag as a concern when:
- login or gating logic is duplicated across many screens
- guarded destinations redirect in ways that lose user context unnecessarily
- success/failure results from intermediate flows are not modeled clearly
8. State hoisting and one-off effects
Navigation is a side effect and should be coordinated with hoisted state.
Check whether:
- navigation-related state is hoisted when multiple composables need to coordinate it
- persistent state is separated from one-time navigation effects
- side effects are not fired from unstable rendering branches
Flag as a concern when:
- navigation decisions are trapped inside leaf composables
- events and persistent UI state are mixed together carelessly
- recomposition can retrigger navigation
9. Deep links
Compose Multiplatform navigation supports destination deep links through NavDeepLink patterns.
Check whether:
- deep-link patterns are explicit and non-overlapping
- placeholders are used intentionally
- required path parameters and optional query parameters are modeled clearly
- the same URI pattern is not claimed by multiple destinations
Flag as a concern when:
- deep-link patterns intersect
- route matching is ambiguous
- deep-link handling depends on accidental ordering
10. Multiplatform routing and browser URL binding
On web, Compose Multiplatform can bind navigation state to browser history and URL fragments.
Check whether:
- browser URL binding happens at the web entry point via
bindToBrowserNavigation() (this API is part of the Compose Multiplatform web-target navigation layer; verify its current stability status against the Compose Multiplatform release notes before treating it as stable API)
- route-to-URL translation is explicit where readability matters
@SerialName or custom route mapping is used when default generated URLs are too implementation-heavy
- browser-specific concerns stay out of generic shared navigation design
Flag as a concern when:
- web URL policy is implicit
- browser-history binding is mixed into unrelated shared UI code
- generated URLs are treated as stable public contracts without review
11. Adaptive navigation UI
Navigation chrome should adapt to available space.
Check whether:
- bottom navigation, rail, drawer, or pane structures are chosen intentionally based on window size
- route state remains stable while shell chrome changes
- adaptive shell behavior does not duplicate destination logic
Flag as a concern when:
- phone navigation chrome is forced unchanged onto large layouts
- shell adaptation and route state are tightly tangled
- large-screen navigation is bolted on with special cases
12. Navigation host architecture
Check whether the navigation host and app shell are decomposed clearly.
Prefer:
- a navigation host or root shell responsible for app-level structure
- destination composables that focus on their own UI/state concerns
- shell chrome separated from destination implementation where practical
Flag as a concern when:
- one root composable owns all destinations, all shell UI, and all navigation logic inline
- host code becomes a monolith
- destination registration and shell layout are tightly tangled
13. Animated transitions
Android’s Navigation docs support configuring transitions between destinations, including enter, exit, pop-enter, and pop-exit transitions.
Check whether:
- transitions support the navigation model instead of obscuring it
- pop transitions reflect back-stack behavior coherently
- animation choices do not fight shared-element or other transition systems
Flag as a concern when:
- transition policy is inconsistent with navigation semantics
- transitions are added everywhere without improving UX clarity
- conflicting transition systems are mixed carelessly
14. Destination isolation
Check whether each destination remains a meaningful UI boundary.
Prefer:
- destination UI that owns its local rendering/state concerns
- dependencies passed through clear contracts
- destination screens that do not know excessive app-shell detail
Flag as a concern when:
- destinations depend directly on root-shell internals
- screens coordinate each other directly instead of going through navigation state
- navigation concerns dominate destination implementation
15. Testability
Navigation architecture should support validation without relying only on full app runs.
Check whether:
- route selection logic can be tested
- navigation state transitions can be tested
- back-stack shaping rules can be validated
- deep-link matching behavior can be tested separately
- destination UI can be previewed or tested in isolation
Flag as a concern when:
- navigation correctness can only be verified through manual app flows
- route transitions are deeply coupled to platform bootstrapping
- deep-link ambiguity is only discovered at runtime
Severity framework
High severity
Likely to cause broken navigation behavior or architectural drift.
Examples:
- no clear
NavController owner
navigate() called from composable rendering paths
- complex objects passed through navigation
- overlapping deep-link patterns
- uncontrolled back-stack mutation from many places
Medium severity
Workable, but likely to create maintenance cost.
Examples:
- route modeling is too stringly typed
- shell and destination responsibilities are too tangled
- back-stack options are used inconsistently
- browser URL mapping is unclear
- adaptive navigation exists but is patchy
Low severity
Structurally acceptable but worth improving.
Examples:
- route naming could be clearer
- transition policy could be more coherent
- shell composables could be split more cleanly
Required output format
When performing the review, respond with:
-
Navigation summary
- route model
NavController / back-stack ownership
- argument strategy
- stack-shaping policy
- deep-link strategy
- adaptive shell approach
- platform/web boundary
-
What is structurally sound
-
Issues by review dimension
- route modeling
NavController ownership
- back stack ownership
- basic navigation policy
- nav options / stack shaping
- argument passing
- conditional navigation
- state hoisting / one-off effects
- deep links
- multiplatform routing / browser binding
- adaptive navigation
- navigation host architecture
- animated transitions
- destination isolation
- testability
-
Severity for each issue
-
Concrete recommendations
- exact restructuring steps
- what state should be hoisted
- what should stop receiving
NavController
- where to use
launchSingleTop / popUpTo / saveState
- how to reduce argument payloads
- how to clarify deep-link and web-routing policy
-
Suggested target structure
- proposed route model / shell / destination / deep-link split if useful
-
Open risks
- migration cost
- rollout concerns
- platform-specific constraints still to validate
Tone
Be direct and practical.
Do not praise navigation just because it works on one device or one happy path.
If the design is weak, say why clearly.
Anti-patterns to flag aggressively
- passing
NavController deep into composables
- calling
navigate() during composition
- stringly typed routes scattered across the codebase
- complex objects passed through navigation
- uncontrolled duplicate destinations on top of the stack
- auth/onboarding guards duplicated across many screens
- overlapping deep-link patterns
- oversized root navigation shells
- browser-routing policy leaking into generic shared navigation code
References