10 KiB
Navigation 3 Parity Strategy (Android + Desktop)
Date: 2026-03-11
Status: Implemented (2026-03-21)
Scope: app and desktop navigation structure using shared core:navigation routes
Context
Desktop and Android both use Navigation 3 typed routes from core:navigation. Previously graph wiring had diverged — desktop used a separate DesktopDestination enum with 6 entries (including a top-level Firmware tab) while Android used 5 entries.
This has been resolved. Both shells now use the shared TopLevelDestination enum from core:navigation/commonMain with 5 entries (Conversations, Nodes, Map, Settings, Connections). Firmware is an in-flow route on both platforms.
Both modules still define separate graph-builder files (app/navigation/*.kt, desktop/navigation/*.kt) with different destination coverage and placeholder behavior, but the top-level shell structure is unified.
Current-State Findings
- Top-level destinations are unified.
- Both shells iterate
TopLevelDestination.entriesfromcore:navigation/commonMain. - Shared icon mapping lives in
core:ui(TopLevelDestinationExt.icon). - Parity tests exist in both
core:navigation/commonTest(NavigationParityTest) anddesktop/test(DesktopTopLevelDestinationParityTest).
- Both shells iterate
- Feature coverage is unified via
commonMainfeature graphs.- The
settingsGraph,nodesGraph,contactsGraph,connectionsGraph,firmwareGraph, andmapGraphare now fully shared and exported from their respective feature modules'commonMainsource sets. - Desktop acts as a thin shell, delegating directly to these shared graphs.
- The
- Saved-state route registration is fully shared.
MeshtasticNavSavedStateConfigincore:navigation/commonMainmaintains the unifiedSavedStateConfigurationserializer list.- Both Android and Desktop reference this shared config when instantiating
rememberNavBackStack.
- Predictive back handling is KMP native.
- Custom
PredictiveBackHandlerwrapper was removed in favor of Jetpack's official KMPNavigationBackHandlerfromandroidx.navigationevent:navigationevent-compose.
- Custom
Alpha04 → Beta01 Changelog Impact Check
Source reviewed: Navigation 3 1.1.0-beta01 (JetBrains fork), CMP 1.11.0-beta01, Lifecycle 2.11.0-alpha02.
Superseded by:
navigation3-api-alignment-2026-03.mdfor the full API surface audit and Scene architecture adoption plan.
- NavDisplay API updated to Scene-based architecture.
- The
sceneStrategy: SceneStrategy<T>parameter is deprecated in favor ofsceneStrategies: List<SceneStrategy<T>>. - New
sceneDecoratorStrategies: List<SceneDecoratorStrategy<T>>parameter available. - New
sharedTransitionScope: SharedTransitionScope?parameter for shared element transitions. - Existing shell patterns in
appanddesktopremain valid using the defaultSinglePaneSceneStrategy.
- The
- Entry-scoped ViewModel lifecycle adopted.
- Both
appanddesktopnow useMeshtasticNavDisplay(core:ui/commonMain), which appliesViewModelStoreNavEntryDecorator+SaveableStateHolderNavEntryDecoratorper active backstack. - ViewModels obtained via
koinViewModel()insideentry<T>blocks are now scoped to the entry's backstack lifetime.
- Both
- No direct Navigation 3 API breakage.
- Release is beta (API stabilized). No migration from alpha04 was required for existing usage patterns.
- Primary risk is dependency wiring drift, not runtime behavior.
- JetBrains Navigation 3 currently publishes
navigation3-uicoordinates (no separatenavigation3-runtimeartifact in Maven Central). Thejetbrains-navigation3-runtimealias intentionally points tonavigation3-uiand is documented in the version catalog. - Note: The
remember*composable factory functions fromnavigation3-runtimeare not visible in non-KMP Android modules due to Kotlin metadata resolution. Use direct class constructors instead (as done inapp/Main.kt).
- JetBrains Navigation 3 currently publishes
- Saved-state and typed-route parity improved.
- Both hosts share
MeshtasticNavSavedStateConfigfromcore:navigation/commonMainviaMultiBackstack, reducing platform drift risk in serializer registration.
- Both hosts share
- Updated active docs to reflect the current dependency baseline (
1.11.0-beta01,1.1.0-beta01,1.3.0-alpha06,2.11.0-alpha02).
Actions Taken
- Renamed all JetBrains-forked lifecycle/nav3 version catalog aliases from
androidx-*tojetbrains-*prefix to make fork provenance unambiguous:jetbrains-lifecycle-runtime,jetbrains-lifecycle-runtime-compose,jetbrains-lifecycle-viewmodel-compose,jetbrains-lifecycle-viewmodel-navigation3jetbrains-navigation3-runtime,jetbrains-navigation3-ui
- Documented in the version catalog that
jetbrains-navigation3-runtimeintentionally maps tonavigation3-uiuntil a separate runtime artifact is published. - Migrated
core:datacommonMainfromandroidx.lifecycle:lifecycle-runtime(Google) toorg.jetbrains.androidx.lifecycle:lifecycle-runtime(JetBrains fork) for full consistency. - Updated active docs to reflect the current dependency baseline (
1.11.0-beta01,1.1.0-beta01,1.3.0-alpha06,2.11.0-alpha02). - Consolidated
appadaptive dependencies to JetBrains Material 3 Adaptive coordinates (org.jetbrains.compose.material3.adaptive:*) so Android and Desktop consume the same adaptive artifact family. The Android-only navigation suite remains onandroidx.compose.material3:material3-adaptive-navigation-suite.
Deferred Follow-ups
- Add automated validation that desktop serializer registrations stay in sync with shared route keys.
Options Evaluated
Option A: Reuse :app navigation implementation directly in desktop
Pros
- Maximum short-term parity in structure.
Cons
:appgraph code is tightly coupled to Android wrappers (Android*ViewModel, Android-only screen wrappers, app-specific UI state like scroll-to-top flows).- Pulling this code into desktop would either fail at compile-time or force additional platform branching in app files.
- Violates clean module boundaries (
desktopshould not depend on Android-specific app glue).
Decision: Not recommended.
Option B: Keep fully separate desktop graph and replicate app behavior manually
Pros
- Lowest refactor cost right now.
- Keeps platform customization simple.
Cons
- Drift is guaranteed over time.
- No central policy for intentional vs accidental divergence.
- High maintenance burden for parity-sensitive flows.
Decision: Not recommended as a long-term strategy.
Option C (Recommended): Hybrid shared contract + platform graph adapters
Pros
- Preserves platform-specific wiring where needed.
- Reduces drift by moving parity-sensitive definitions to shared contracts.
- Enables explicit, testable exceptions for desktop-only or Android-only behavior.
Cons
- Requires incremental extraction work.
- Needs light governance (parity matrix + tests + docs).
Decision: Recommended.
Decision
Adopt a hybrid parity model:
- Keep platform graph registration in
appanddesktop. - Extract parity-sensitive navigation metadata into shared contracts (top-level destination set/order, route ownership map, and allowed platform exceptions).
- Keep platform-specific destination implementations as adapters around shared route keys.
- Add route parity tests so drift is detected automatically.
Implementation Plan
Phase 1 (Immediate): Stop drift on shell structure ✅
- ✅ Aligned desktop top-level destination policy with Android (removed Firmware from top-level; kept as in-flow).
- ✅ Both shells now use shared
TopLevelDestinationenum fromcore:navigation/commonMain. - ✅ Shared icon mapping in
core:ui(TopLevelDestinationExt.icon). - Parity matrix documented inline: top-level set is Conversations, Nodes, Map, Settings, Connections on both platforms.
Phase 2 (Near-term): Extract shared navigation contracts ✅ (partially)
- ✅ Shared
TopLevelDestinationenum withfromNavKey()already serves as the canonical metadata object. - Both
appanddesktopshells iterateTopLevelDestination.entries— no separateDesktopDestinationenum remains. - Remaining: optional visibility flags by platform, route grouping metadata (lower priority since shells are unified).
Phase 3 (Near-term): Add parity checks ✅ (partially)
- ✅
NavigationParityTestincore:navigation/commonTest— asserts 5 top-level destinations andfromNavKeymatching. - ✅
DesktopTopLevelDestinationParityTestindesktop/test— asserts desktop routes match Android parity set and firmware is not top-level. - Remaining: assert every desktop serializer registration corresponds to an actual route; assert every intentional exception is listed.
Phase 4 (Mid-term): Reduce app-specific graph coupling
- Move reusable graph composition helpers out of
:appwhere practical (while keeping Android-only wrappers in Android source sets). - Keep desktop-specific placeholder implementations, but tie them to explicit parity exception entries.
Consequences
- Navigation behavior remains platform-adaptive, but parity expectations become explicit and enforceable.
- Desktop can keep legitimate deviations (map/charts/platform integrations) without silently changing IA.
- New route additions will require touching one shared contract plus platform implementations, making review scope clearer.
Source Anchors
- Shared routes:
core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt - Shared saved-state config:
core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt - Android shell:
app/src/main/kotlin/org/meshtastic/app/ui/Main.kt - Shared graph registrations:
feature/*/src/commonMain/kotlin/org/meshtastic/feature/*/navigation/ - Platform graph content:
feature/*/src/{androidMain,jvmMain}/kotlin/org/meshtastic/feature/*/navigation/ - Desktop shell:
desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt - Desktop graph assembly:
desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt