refactor: adaptive UI components for Navigation 3 (#4891)

This commit is contained in:
James Rich 2026-03-23 12:35:02 -05:00 committed by GitHub
parent b3b38acc0b
commit 7b327215f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 978 additions and 1751 deletions

View file

@ -17,21 +17,12 @@
package org.meshtastic.feature.node.navigation
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.rememberListDetailPaneScaffoldNavigator
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.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigationevent.NavigationEventInfo
import androidx.navigationevent.compose.NavigationBackHandler
import androidx.navigationevent.compose.rememberNavigationEventState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@ -41,6 +32,7 @@ import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.nodes
import org.meshtastic.core.ui.component.AdaptiveListDetailScaffold
import org.meshtastic.core.ui.component.EmptyDetailPlaceholder
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.icon.MeshtasticIcons
@ -64,9 +56,8 @@ fun AdaptiveNodeListScreen(
val nodeListViewModel: NodeListViewModel = koinViewModel()
val navigator = rememberListDetailPaneScaffoldNavigator<Int>()
val scope = rememberCoroutineScope()
val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
val handleBack: () -> Unit = {
val onBackToGraph: () -> Unit = {
val currentKey = backStack.lastOrNull()
val isNodesRoute = currentKey is NodesRoutes.Nodes || currentKey is NodesRoutes.NodesGraph
val previousKey = if (backStack.size > 1) backStack[backStack.size - 2] else null
@ -76,80 +67,40 @@ fun AdaptiveNodeListScreen(
if (isFromDifferentGraph && !isNodesRoute) {
// Navigate back via NavController to return to the previous screen
backStack.removeLastOrNull()
} else {
// Close the detail pane within the adaptive scaffold
scope.launch { navigator.navigateBack(backNavigationBehavior) }
}
}
val navState = rememberNavigationEventState(NavigationEventInfo.None)
NavigationBackHandler(
state = navState,
isBackEnabled = navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail,
onBackCancelled = {},
onBackCompleted = { handleBack() },
)
LaunchedEffect(initialNodeId) {
if (initialNodeId != null) {
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, initialNodeId)
}
}
LaunchedEffect(scrollToTopEvents) {
scrollToTopEvents.collect { event ->
if (
event is ScrollToTopEvent.NodesTabPressed &&
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() }
NodeListScreen(
viewModel = nodeListViewModel,
navigateToNodeDetails = { nodeId ->
scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, nodeId) }
},
onNavigateToChannels = { backStack.add(ChannelsRoutes.ChannelsGraph) },
scrollToTopEvents = scrollToTopEvents,
activeNodeId = navigator.currentDestination?.contentKey,
)
}
AdaptiveListDetailScaffold(
navigator = navigator,
scrollToTopEvents = scrollToTopEvents,
onBackToGraph = onBackToGraph,
onTabPressedEvent = { it is ScrollToTopEvent.NodesTabPressed },
initialKey = initialNodeId,
listPane = { isActive, activeNodeId ->
NodeListScreen(
viewModel = nodeListViewModel,
navigateToNodeDetails = { nodeId ->
scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, nodeId) }
},
onNavigateToChannels = { backStack.add(ChannelsRoutes.ChannelsGraph) },
scrollToTopEvents = scrollToTopEvents,
activeNodeId = activeNodeId,
)
},
detailPane = {
AnimatedPane {
val focusManager = LocalFocusManager.current
// Prevent TextFields from auto-focusing when pane animates in
navigator.currentDestination?.contentKey?.let { nodeId ->
key(nodeId) {
LaunchedEffect(nodeId) { focusManager.clearFocus() }
val nodeDetailViewModel: NodeDetailViewModel = koinViewModel()
val compassViewModel: CompassViewModel = koinViewModel()
NodeDetailScreen(
nodeId = nodeId,
viewModel = nodeDetailViewModel,
compassViewModel = compassViewModel,
navigateToMessages = onNavigateToMessages,
onNavigate = onNavigate,
onNavigateUp = handleBack,
)
}
} ?: EmptyDetailPlaceholder(icon = MeshtasticIcons.Nodes, title = stringResource(Res.string.nodes))
}
detailPane = { contentKey, handleBack ->
val nodeDetailViewModel: NodeDetailViewModel = koinViewModel()
val compassViewModel: CompassViewModel = koinViewModel()
NodeDetailScreen(
nodeId = contentKey,
viewModel = nodeDetailViewModel,
compassViewModel = compassViewModel,
navigateToMessages = onNavigateToMessages,
onNavigate = onNavigate,
onNavigateUp = handleBack,
)
},
emptyDetailPane = {
EmptyDetailPlaceholder(icon = MeshtasticIcons.Nodes, title = stringResource(Res.string.nodes))
},
)
}