mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: migrate list-detail layouts to Material 3 Adaptive Navigation3
- Replace the custom `AdaptiveListDetailScaffold` with the official `ListDetailSceneStrategy` to manage adaptive layout orchestration. - Integrate `ListDetailSceneStrategy` into `MeshtasticNavDisplay` to enable native pane switching based on the navigation backstack. - Update `ContactsNavigation` and `NodesNavigation` graphs to include `listPane()` and `detailPane()` metadata for relevant route entries. - Simplify `AdaptiveContactsScreen` and `AdaptiveNodeListScreen` by removing manual scaffold navigation logic and delegating to the navigation framework. - Add the `jetbrains.compose.material3.adaptive.navigation3` library dependency to `core:ui` and relevant feature modules. - Refactor `DesktopMainScreen` and `Main.kt` with minor formatting and indentation updates for better readability.
This commit is contained in:
parent
26aa8377c5
commit
9c9a1d7567
12 changed files with 72 additions and 301 deletions
|
|
@ -57,6 +57,7 @@ kotlin {
|
|||
implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite)
|
||||
implementation(libs.jetbrains.navigationevent.compose)
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation3)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
/*
|
||||
* 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.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.AnimatedPane
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.navigationevent.NavigationEventInfo
|
||||
import androidx.navigationevent.compose.NavigationBackHandler
|
||||
import androidx.navigationevent.compose.rememberNavigationEventState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun <T> AdaptiveListDetailScaffold(
|
||||
navigator: ThreePaneScaffoldNavigator<T>,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>,
|
||||
onBackToGraph: () -> Unit,
|
||||
onTabPressedEvent: (ScrollToTopEvent) -> Boolean,
|
||||
initialKey: T? = null,
|
||||
listPane: @Composable (isActive: Boolean, contentKey: T?) -> Unit,
|
||||
detailPane: @Composable (contentKey: T, handleBack: () -> Unit) -> Unit,
|
||||
emptyDetailPane: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
|
||||
|
||||
val handleBack: () -> Unit = {
|
||||
if (navigator.canNavigateBack(backNavigationBehavior)) {
|
||||
scope.launch { navigator.navigateBack(backNavigationBehavior) }
|
||||
} else {
|
||||
onBackToGraph()
|
||||
}
|
||||
}
|
||||
|
||||
val navState = rememberNavigationEventState(NavigationEventInfo.None)
|
||||
NavigationBackHandler(
|
||||
state = navState,
|
||||
isBackEnabled = navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail,
|
||||
onBackCancelled = {},
|
||||
onBackCompleted = { handleBack() },
|
||||
)
|
||||
|
||||
LaunchedEffect(initialKey) {
|
||||
if (initialKey != null) {
|
||||
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, initialKey)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(scrollToTopEvents) {
|
||||
scrollToTopEvents.collect { event ->
|
||||
if (onTabPressedEvent(event) && navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail) {
|
||||
if (navigator.canNavigateBack(backNavigationBehavior)) {
|
||||
navigator.navigateBack(backNavigationBehavior)
|
||||
} else {
|
||||
navigator.navigateTo(ListDetailPaneScaffoldRole.List)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListDetailPaneScaffold(
|
||||
directive = navigator.scaffoldDirective,
|
||||
value = navigator.scaffoldValue,
|
||||
listPane = {
|
||||
AnimatedPane {
|
||||
val focusManager = LocalFocusManager.current
|
||||
// Prevent TextFields from auto-focusing when pane animates in
|
||||
LaunchedEffect(Unit) { focusManager.clearFocus() }
|
||||
|
||||
listPane(
|
||||
navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.List,
|
||||
navigator.currentDestination?.contentKey,
|
||||
)
|
||||
}
|
||||
},
|
||||
detailPane = {
|
||||
AnimatedPane {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
navigator.currentDestination?.contentKey?.let { contentKey ->
|
||||
key(contentKey) {
|
||||
LaunchedEffect(contentKey) { focusManager.clearFocus() }
|
||||
detailPane(contentKey, handleBack)
|
||||
}
|
||||
} ?: emptyDetailPane()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@ import androidx.compose.animation.ContentTransform
|
|||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
|
|
@ -52,6 +54,8 @@ private const val TRANSITION_DURATION_MS = 350
|
|||
* **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.
|
||||
* - [ListDetailSceneStrategy] — entries annotated with `listPane()` / `detailPane()` render in adaptive list-detail
|
||||
* layout on wider screens.
|
||||
* - [SinglePaneSceneStrategy] — default single-pane fallback.
|
||||
*
|
||||
* **Transitions**: A uniform 350 ms crossfade for both forward and pop navigation.
|
||||
|
|
@ -60,18 +64,20 @@ private const val TRANSITION_DURATION_MS = 350
|
|||
* @param entryProvider the entry provider built from feature navigation graphs.
|
||||
* @param modifier modifier applied to the underlying [NavDisplay].
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun MeshtasticNavDisplay(
|
||||
backStack: List<NavKey>,
|
||||
entryProvider: (key: NavKey) -> NavEntry<NavKey>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listDetailSceneStrategy = rememberListDetailSceneStrategy<NavKey>()
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
entryProvider = entryProvider,
|
||||
entryDecorators =
|
||||
listOf(rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator()),
|
||||
sceneStrategies = listOf(DialogSceneStrategy(), SinglePaneSceneStrategy()),
|
||||
sceneStrategies = listOf(DialogSceneStrategy(), listDetailSceneStrategy, SinglePaneSceneStrategy()),
|
||||
transitionSpec = meshtasticTransitionSpec(),
|
||||
popTransitionSpec = meshtasticTransitionSpec(),
|
||||
modifier = modifier,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue