feat: implement MeshtasticNavDisplay and centralize Navigation 3 configuration

- Introduce `MeshtasticNavDisplay` in `core:ui` to encapsulate shared Navigation 3 configuration, including entry decorators, scene strategies, and transitions.
- Integrate `rememberViewModelStoreNavEntryDecorator` to provide entry-scoped `ViewModelStoreOwner` support, ensuring ViewModels are automatically cleared when their backstack entry is popped.
- Configure `DialogSceneStrategy` to enable navigation-driven dialogs that respect backstack lifecycle and predictive back gestures.
- Implement a standardized 350 ms crossfade transition for both forward and pop navigation across all platforms.
- Refactor `app` and `desktop` host modules to utilize `MeshtasticNavDisplay`, removing redundant manual configuration of `NavDisplay` and decorators.
- Update `core:ui` dependencies to include `lifecycle-viewmodel-navigation3` and move it away from platform-specific host modules.
- Update architectural documentation (`AGENTS.md`, `GEMINI.md`, and decision logs) to reflect the adoption of centralized navigation and ViewModel scoping patterns.
This commit is contained in:
James Rich 2026-03-26 13:54:27 -05:00
parent 37729c13d8
commit 829aecd888
10 changed files with 122 additions and 57 deletions

View file

@ -57,6 +57,8 @@ kotlin {
implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite)
implementation(libs.jetbrains.navigationevent.compose)
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
}
val jvmAndroidMain by getting { dependencies { implementation(libs.compose.multiplatform.ui.tooling) } }

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.scene.DialogSceneStrategy
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SinglePaneSceneStrategy
import androidx.navigation3.ui.NavDisplay
/**
* Duration in milliseconds for the shared crossfade transition between navigation scenes.
*
* This is faster than the library's Android default (700 ms) and matches Material 3 motion guidance for medium-emphasis
* container transforms (~300-400 ms).
*/
private const val TRANSITION_DURATION_MS = 350
/**
* Shared [NavDisplay] wrapper that configures the standard Meshtastic entry decorators, scene strategies, and
* transition animations for all platform hosts.
*
* **Entry decorators** (applied to every backstack entry):
* - [rememberSaveableStateHolderNavEntryDecorator] saveable state per entry.
* - [rememberViewModelStoreNavEntryDecorator] entry-scoped `ViewModelStoreOwner` so that ViewModels obtained via
* `koinViewModel()` are automatically cleared when the entry is popped.
*
* **Scene strategies** (evaluated in order):
* - [DialogSceneStrategy] entries annotated with `metadata = DialogSceneStrategy.dialog()` render as overlay
* [Dialog][androidx.compose.ui.window.Dialog] windows with proper backstack lifecycle.
* - [SinglePaneSceneStrategy] default single-pane fallback.
*
* **Transitions**: A uniform 350 ms crossfade for both forward and pop navigation.
*
* @param backStack the navigation backstack, typically from [rememberNavBackStack].
* @param entryProvider the entry provider built from feature navigation graphs.
* @param modifier modifier applied to the underlying [NavDisplay].
*/
@Composable
fun MeshtasticNavDisplay(
backStack: List<NavKey>,
entryProvider: (key: NavKey) -> NavEntry<NavKey>,
modifier: Modifier = Modifier,
) {
NavDisplay(
backStack = backStack,
entryProvider = entryProvider,
entryDecorators =
listOf(rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator()),
sceneStrategies = listOf(DialogSceneStrategy(), SinglePaneSceneStrategy()),
transitionSpec = meshtasticTransitionSpec(),
popTransitionSpec = meshtasticTransitionSpec(),
modifier = modifier,
)
}
/**
* Shared crossfade [ContentTransform] used for both forward and pop navigation. Returns a lambda compatible with
* [NavDisplay]'s `transitionSpec` / `popTransitionSpec` parameters.
*/
private fun meshtasticTransitionSpec(): AnimatedContentTransitionScope<Scene<NavKey>>.() -> ContentTransform = {
ContentTransform(
fadeIn(animationSpec = tween(TRANSITION_DURATION_MS)),
fadeOut(animationSpec = tween(TRANSITION_DURATION_MS)),
)
}