Meshtastic-Android/docs/decisions/navigation3-parity-2026-03.md

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

  1. Top-level destinations are unified.
    • Both shells iterate TopLevelDestination.entries from core:navigation/commonMain.
    • Shared icon mapping lives in core:ui (TopLevelDestinationExt.icon).
    • Parity tests exist in both core:navigation/commonTest (NavigationParityTest) and desktop/test (DesktopTopLevelDestinationParityTest).
  2. Feature coverage is unified via commonMain feature graphs.
    • The settingsGraph, nodesGraph, contactsGraph, connectionsGraph, firmwareGraph, and mapGraph are now fully shared and exported from their respective feature modules' commonMain source sets.
    • Desktop acts as a thin shell, delegating directly to these shared graphs.
  3. Saved-state route registration is fully shared.
    • MeshtasticNavSavedStateConfig in core:navigation/commonMain maintains the unified SavedStateConfiguration serializer list.
    • Both Android and Desktop reference this shared config when instantiating rememberNavBackStack.
  4. Predictive back handling is KMP native.
    • Custom PredictiveBackHandler wrapper was removed in favor of Jetpack's official KMP NavigationBackHandler from androidx.navigationevent:navigationevent-compose.

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.md for the full API surface audit and Scene architecture adoption plan.

  1. NavDisplay API updated to Scene-based architecture.
    • The sceneStrategy: SceneStrategy<T> parameter is deprecated in favor of sceneStrategies: List<SceneStrategy<T>>.
    • New sceneDecoratorStrategies: List<SceneDecoratorStrategy<T>> parameter available.
    • New sharedTransitionScope: SharedTransitionScope? parameter for shared element transitions.
    • Existing shell patterns in app and desktop remain valid using the default SinglePaneSceneStrategy.
  2. Entry-scoped ViewModel lifecycle adopted.
    • Both app and desktop now use MeshtasticNavDisplay (core:ui/commonMain), which applies ViewModelStoreNavEntryDecorator + SaveableStateHolderNavEntryDecorator per active backstack.
    • ViewModels obtained via koinViewModel() inside entry<T> blocks are now scoped to the entry's backstack lifetime.
  3. No direct Navigation 3 API breakage.
    • Release is beta (API stabilized). No migration from alpha04 was required for existing usage patterns.
  4. Primary risk is dependency wiring drift, not runtime behavior.
    • JetBrains Navigation 3 currently publishes navigation3-ui coordinates (no separate navigation3-runtime artifact in Maven Central). The jetbrains-navigation3-runtime alias intentionally points to navigation3-ui and is documented in the version catalog.
    • Note: The remember* composable factory functions from navigation3-runtime are not visible in non-KMP Android modules due to Kotlin metadata resolution. Use direct class constructors instead (as done in app/Main.kt).
  5. Saved-state and typed-route parity improved.
    • Both hosts share MeshtasticNavSavedStateConfig from core:navigation/commonMain via MultiBackstack, reducing platform drift risk in serializer registration.
  6. 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-* to jetbrains-* prefix to make fork provenance unambiguous:
    • jetbrains-lifecycle-runtime, jetbrains-lifecycle-runtime-compose, jetbrains-lifecycle-viewmodel-compose, jetbrains-lifecycle-viewmodel-navigation3
    • jetbrains-navigation3-runtime, jetbrains-navigation3-ui
  • Documented in the version catalog that jetbrains-navigation3-runtime intentionally maps to navigation3-ui until a separate runtime artifact is published.
  • Migrated core:data commonMain from androidx.lifecycle:lifecycle-runtime (Google) to org.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 app adaptive 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 on androidx.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

  • :app graph 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 (desktop should 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.

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:

  1. Keep platform graph registration in app and desktop.
  2. Extract parity-sensitive navigation metadata into shared contracts (top-level destination set/order, route ownership map, and allowed platform exceptions).
  3. Keep platform-specific destination implementations as adapters around shared route keys.
  4. 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 TopLevelDestination enum from core: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 TopLevelDestination enum with fromNavKey() already serves as the canonical metadata object.
  • Both app and desktop shells iterate TopLevelDestination.entries — no separate DesktopDestination enum remains.
  • Remaining: optional visibility flags by platform, route grouping metadata (lower priority since shells are unified).

Phase 3 (Near-term): Add parity checks (partially)

  • NavigationParityTest in core:navigation/commonTest — asserts 5 top-level destinations and fromNavKey matching.
  • DesktopTopLevelDestinationParityTest in desktop/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 :app where 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