mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-28 02:32:24 -04:00
refactor: improve navigation backstack handling and screen decoupling
This commit refactors the navigation logic across `feature:node` and `feature:messaging` to use more robust backstack manipulation helpers and decouple screen implementations from navigation parameters.
Specific changes include:
- **Navigation Helpers**: Added `replaceLast` and `replaceAll` extensions to `MutableList<NavKey>` in `core:navigation` to allow more granular control over backstack mutations while minimizing structural changes.
- **Node Feature**:
- Updated `NodesRoutes.NodeDetail` entry to directly instantiate `NodeDetailViewModel` and `CompassViewModel` using Koin, removing the reliance on `AdaptiveNodeListScreen` for detail rendering.
- Simplified `AdaptiveNodeListScreen` by removing unused navigation and initialization parameters.
- **Messaging Feature**:
- Refactored `ContactsRoutes.Messages` entry to handle ViewModel lifecycle and contact key initialization directly within the navigation graph.
- Updated `ShareScreen` to use the new `replaceLast` helper for smoother transitions to message screens.
- Simplified `AdaptiveContactsScreen` and `ContactsEntryContent` by removing unused `detailPaneCustom` and initialization logic.
- **Deep Linking**: Updated `MeshtasticAppShell` to use `backStack.replaceAll(navKeys)` when handling deep links, ensuring a cleaner backstack state transition.
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -24,7 +24,9 @@ import androidx.navigation3.runtime.NavKey
|
||||
*/
|
||||
fun MutableList<NavKey>.navigateTopLevel(route: NavKey) {
|
||||
if (isNotEmpty()) {
|
||||
this[0] = route
|
||||
if (this[0] != route) {
|
||||
this[0] = route
|
||||
}
|
||||
while (size > 1) {
|
||||
removeAt(lastIndex)
|
||||
}
|
||||
@@ -32,3 +34,41 @@ fun MutableList<NavKey>.navigateTopLevel(route: NavKey) {
|
||||
add(route)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the last entry in the back stack with the given route.
|
||||
* If the back stack is empty, it simply adds the route.
|
||||
*/
|
||||
fun MutableList<NavKey>.replaceLast(route: NavKey) {
|
||||
if (isNotEmpty()) {
|
||||
if (this[lastIndex] != route) {
|
||||
this[lastIndex] = route
|
||||
}
|
||||
} else {
|
||||
add(route)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the entire back stack with the given routes in a way that minimizes structural changes
|
||||
* and prevents the back stack from temporarily becoming empty.
|
||||
*/
|
||||
fun MutableList<NavKey>.replaceAll(routes: List<NavKey>) {
|
||||
if (routes.isEmpty()) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
for (i in routes.indices) {
|
||||
if (i < size) {
|
||||
// Only mutate if the route actually changed, protecting Nav3's internal state matching.
|
||||
if (this[i] != routes[i]) {
|
||||
this[i] = routes[i]
|
||||
}
|
||||
} else {
|
||||
add(routes[i])
|
||||
}
|
||||
}
|
||||
while (size > routes.size) {
|
||||
removeAt(lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.replaceAll
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
|
||||
/**
|
||||
@@ -39,8 +40,7 @@ fun MeshtasticAppShell(
|
||||
) {
|
||||
LaunchedEffect(uiViewModel) {
|
||||
uiViewModel.navigationDeepLink.collect { navKeys ->
|
||||
backStack.clear()
|
||||
backStack.addAll(navKeys)
|
||||
backStack.replaceAll(navKeys)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.replaceLast
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.messaging.QuickChatScreen
|
||||
import org.meshtastic.feature.messaging.QuickChatViewModel
|
||||
@@ -50,11 +51,20 @@ fun EntryProviderScope<NavKey>.contactsGraph(
|
||||
}
|
||||
|
||||
entry<ContactsRoutes.Messages>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
|
||||
ContactsEntryContent(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
initialContactKey = args.contactKey,
|
||||
initialMessage = args.message,
|
||||
val contactKey = args.contactKey
|
||||
val messageViewModel: org.meshtastic.feature.messaging.MessageViewModel =
|
||||
koinViewModel(key = "messages-$contactKey")
|
||||
messageViewModel.setContactKey(contactKey)
|
||||
|
||||
org.meshtastic.feature.messaging.MessageScreen(
|
||||
contactKey = contactKey,
|
||||
message = args.message,
|
||||
viewModel = messageViewModel,
|
||||
navigateToNodeDetails = {
|
||||
backStack.add(org.meshtastic.core.navigation.NodesRoutes.NodeDetailGraph(it))
|
||||
},
|
||||
navigateToQuickChatOptions = { backStack.add(org.meshtastic.core.navigation.ContactsRoutes.QuickChat) },
|
||||
onNavigateBack = { backStack.removeLastOrNull() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,10 +73,8 @@ fun EntryProviderScope<NavKey>.contactsGraph(
|
||||
val viewModel = koinViewModel<ContactsViewModel>()
|
||||
ShareScreen(
|
||||
viewModel = viewModel,
|
||||
onConfirm = {
|
||||
// Navigation 3 - replace Top with Messages manually, but for now we just pop and add
|
||||
backStack.removeLastOrNull()
|
||||
backStack.add(ContactsRoutes.Messages(it, message))
|
||||
onConfirm = { contactKey ->
|
||||
backStack.replaceLast(ContactsRoutes.Messages(contactKey, message))
|
||||
},
|
||||
onNavigateUp = { backStack.removeLastOrNull() },
|
||||
)
|
||||
@@ -82,8 +90,6 @@ fun EntryProviderScope<NavKey>.contactsGraph(
|
||||
fun ContactsEntryContent(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>,
|
||||
initialContactKey: String? = null,
|
||||
initialMessage: String = "",
|
||||
) {
|
||||
val uiViewModel: org.meshtastic.core.ui.viewmodel.UIViewModel = koinViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
@@ -93,30 +99,11 @@ fun ContactsEntryContent(
|
||||
AdaptiveContactsScreen(
|
||||
backStack = backStack,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = koinViewModel(), // Ignored by custom detail pane below
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
onHandleDeepLink = uiViewModel::handleDeepLink,
|
||||
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
|
||||
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
|
||||
initialContactKey = initialContactKey,
|
||||
initialMessage = initialMessage,
|
||||
detailPaneCustom = { contactKey ->
|
||||
val messageViewModel: org.meshtastic.feature.messaging.MessageViewModel =
|
||||
koinViewModel(key = "messages-$contactKey")
|
||||
messageViewModel.setContactKey(contactKey)
|
||||
|
||||
org.meshtastic.feature.messaging.MessageScreen(
|
||||
contactKey = contactKey,
|
||||
message = if (contactKey == initialContactKey) initialMessage else "",
|
||||
viewModel = messageViewModel,
|
||||
navigateToNodeDetails = {
|
||||
backStack.add(org.meshtastic.core.navigation.NodesRoutes.NodeDetailGraph(it))
|
||||
},
|
||||
navigateToQuickChatOptions = { backStack.add(org.meshtastic.core.navigation.ContactsRoutes.QuickChat) },
|
||||
onNavigateBack = { backStack.removeLastOrNull() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.meshtastic.core.navigation.ChannelsRoutes
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.messaging.MessageViewModel
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
||||
@@ -33,16 +32,12 @@ import org.meshtastic.proto.SharedContact
|
||||
fun AdaptiveContactsScreen(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
contactsViewModel: ContactsViewModel,
|
||||
messageViewModel: MessageViewModel,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>,
|
||||
sharedContactRequested: SharedContact?,
|
||||
requestChannelSet: ChannelSet?,
|
||||
onHandleDeepLink: (MeshtasticUri, onInvalid: () -> Unit) -> Unit,
|
||||
onClearSharedContactRequested: () -> Unit,
|
||||
onClearRequestChannelUrl: () -> Unit,
|
||||
initialContactKey: String? = null,
|
||||
initialMessage: String = "",
|
||||
detailPaneCustom: @Composable ((contactKey: String) -> Unit)? = null,
|
||||
) {
|
||||
ContactsScreen(
|
||||
onNavigateToShare = { backStack.add(ChannelsRoutes.ChannelsGraph) },
|
||||
|
||||
@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
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.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.node.list.NodeListScreen
|
||||
import org.meshtastic.feature.node.list.NodeListViewModel
|
||||
@@ -32,9 +31,6 @@ import org.meshtastic.feature.node.list.NodeListViewModel
|
||||
fun AdaptiveNodeListScreen(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>,
|
||||
initialNodeId: Int? = null,
|
||||
onNavigate: (Route) -> Unit = {},
|
||||
onNavigateToMessages: (String) -> Unit = {},
|
||||
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
|
||||
) {
|
||||
val nodeListViewModel: NodeListViewModel = koinViewModel()
|
||||
|
||||
@@ -53,6 +53,9 @@ import org.meshtastic.core.resources.power
|
||||
import org.meshtastic.core.resources.signal
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
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.metrics.DeviceMetricsScreen
|
||||
import org.meshtastic.feature.node.metrics.EnvironmentMetricsScreen
|
||||
import org.meshtastic.feature.node.metrics.HostMetricsLogScreen
|
||||
@@ -76,8 +79,6 @@ fun EntryProviderScope<NavKey>.nodesGraph(
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
onNavigate = { backStack.add(it) },
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
onHandleDeepLink = onHandleDeepLink,
|
||||
)
|
||||
}
|
||||
@@ -86,8 +87,6 @@ fun EntryProviderScope<NavKey>.nodesGraph(
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
onNavigate = { backStack.add(it) },
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
onHandleDeepLink = onHandleDeepLink,
|
||||
)
|
||||
}
|
||||
@@ -106,21 +105,21 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
initialNodeId = args.destNum,
|
||||
onNavigate = { backStack.add(it) },
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
onHandleDeepLink = onHandleDeepLink,
|
||||
)
|
||||
}
|
||||
|
||||
entry<NodesRoutes.NodeDetail>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
initialNodeId = args.destNum,
|
||||
val nodeDetailViewModel: NodeDetailViewModel = koinViewModel()
|
||||
val compassViewModel: CompassViewModel = koinViewModel()
|
||||
val destNum = args.destNum ?: 0 // Handle nullable destNum if needed
|
||||
NodeDetailScreen(
|
||||
nodeId = destNum,
|
||||
viewModel = nodeDetailViewModel,
|
||||
compassViewModel = compassViewModel,
|
||||
navigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
onNavigate = { backStack.add(it) },
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
onHandleDeepLink = onHandleDeepLink,
|
||||
onNavigateUp = { backStack.removeLastOrNull() },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user