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:
James Rich 2026-03-26 16:52:08 -05:00
parent 26aa8377c5
commit 9c9a1d7567
12 changed files with 72 additions and 301 deletions

View file

@ -59,6 +59,7 @@ kotlin {
implementation(libs.jetbrains.compose.material3.adaptive)
implementation(libs.jetbrains.compose.material3.adaptive.layout)
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
implementation(libs.jetbrains.compose.material3.adaptive.navigation3)
}
androidMain.dependencies {

View file

@ -16,35 +16,18 @@
*/
package org.meshtastic.feature.node.navigation
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.ChannelsRoutes
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
import org.meshtastic.core.ui.icon.Nodes
import org.meshtastic.feature.node.compass.CompassViewModel
import org.meshtastic.feature.node.detail.NodeDetailScreen
import org.meshtastic.feature.node.detail.NodeDetailViewModel
import org.meshtastic.feature.node.list.NodeListScreen
import org.meshtastic.feature.node.list.NodeListViewModel
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun AdaptiveNodeListScreen(
backStack: NavBackStack<NavKey>,
@ -55,54 +38,13 @@ fun AdaptiveNodeListScreen(
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
val nodeListViewModel: NodeListViewModel = koinViewModel()
val navigator = rememberListDetailPaneScaffoldNavigator<Int>()
val scope = rememberCoroutineScope()
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
val isFromDifferentGraph =
previousKey != null && previousKey !is NodesRoutes.NodesGraph && previousKey !is NodesRoutes.Nodes
if (isFromDifferentGraph && !isNodesRoute) {
// Navigate back via NavController to return to the previous screen
backStack.removeLastOrNull()
}
}
AdaptiveListDetailScaffold(
navigator = navigator,
NodeListScreen(
viewModel = nodeListViewModel,
navigateToNodeDetails = { nodeId -> backStack.add(NodesRoutes.NodeDetail(nodeId)) },
onNavigateToChannels = { backStack.add(ChannelsRoutes.ChannelsGraph) },
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,
onHandleDeepLink = onHandleDeepLink,
)
},
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))
},
activeNodeId = null,
onHandleDeepLink = onHandleDeepLink,
)
}

View file

@ -26,6 +26,8 @@ import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.PermScanWifi
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.Router
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation3.runtime.EntryProviderScope
@ -63,13 +65,14 @@ import org.meshtastic.feature.node.metrics.SignalMetricsScreen
import org.meshtastic.feature.node.metrics.TracerouteLogScreen
import kotlin.reflect.KClass
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Suppress("LongMethod")
fun EntryProviderScope<NavKey>.nodesGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent> = MutableSharedFlow(),
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
entry<NodesRoutes.NodesGraph> {
entry<NodesRoutes.NodesGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
@ -79,7 +82,7 @@ fun EntryProviderScope<NavKey>.nodesGraph(
)
}
entry<NodesRoutes.Nodes> {
entry<NodesRoutes.Nodes>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
@ -92,13 +95,14 @@ fun EntryProviderScope<NavKey>.nodesGraph(
nodeDetailGraph(backStack, scrollToTopEvents, onHandleDeepLink)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Suppress("LongMethod")
fun EntryProviderScope<NavKey>.nodeDetailGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
entry<NodesRoutes.NodeDetailGraph> { args ->
entry<NodesRoutes.NodeDetailGraph>(metadata = { ListDetailSceneStrategy.listPane() }) { args ->
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,
@ -109,7 +113,7 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
)
}
entry<NodesRoutes.NodeDetail> { args ->
entry<NodesRoutes.NodeDetail>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
AdaptiveNodeListScreen(
backStack = backStack,
scrollToTopEvents = scrollToTopEvents,