refactor: organize navigation (#2042)

This commit is contained in:
James Rich 2025-06-06 20:45:26 +00:00 committed by GitHub
parent c757224269
commit 2a05fc072d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 306 additions and 265 deletions

View file

@ -24,13 +24,13 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.sharing.ChannelScreen
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
import com.geeksville.mesh.ui.sharing.ChannelScreen
import kotlinx.serialization.Serializable
@Serializable
sealed interface ChannelsRoutes {
sealed class ChannelsRoutes {
@Serializable
data object ChannelsGraph : Graph

View file

@ -30,7 +30,7 @@ import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
import kotlinx.serialization.Serializable
@Serializable
sealed interface ConnectionsRoutes {
sealed class ConnectionsRoutes {
@Serializable
data object ConnectionsGraph : Graph
@ -59,8 +59,8 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController, uiViewMod
ConnectionsScreen(
uiViewModel,
radioConfigViewModel = hiltViewModel(parentEntry),
onNavigateToRadioConfig = { navController.navigate(Route.RadioConfig()) },
onNavigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
onNavigateToRadioConfig = { navController.navigate(RadioConfigRoutes.RadioConfig()) },
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetail(it)) },
onConfigNavigate = { route -> navController.navigate(route) }
)
}
@ -71,15 +71,10 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController, uiViewMod
private fun NavGraphBuilder.configRoutes(
navController: NavHostController,
) {
ConfigRoute.entries.forEach { configRoute ->
composable(configRoute.route::class) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<ConnectionsRoutes.ConnectionsGraph>()
}
when (configRoute) {
ConfigRoute.LORA -> LoRaConfigScreen(hiltViewModel(parentEntry))
else -> Unit
}
composable<RadioConfigRoutes.LoRa> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<ConnectionsRoutes.ConnectionsGraph>()
}
LoRaConfigScreen(hiltViewModel(parentEntry))
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import androidx.navigation.toRoute
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.contact.ContactsScreen
import com.geeksville.mesh.ui.message.MessageScreen
import com.geeksville.mesh.ui.message.QuickChatScreen
import com.geeksville.mesh.ui.sharing.ShareScreen
import kotlinx.serialization.Serializable
sealed class ContactsRoutes {
@Serializable
data object Contacts : Route
@Serializable
data class Messages(val contactKey: String, val message: String = "") : Route
@Serializable
data class Share(val message: String) : Route
@Serializable
data object QuickChat : Route
@Serializable
data object ContactsGraph : Graph
}
fun NavGraphBuilder.contactsGraph(
navController: NavHostController,
uiViewModel: UIViewModel,
) {
navigation<ContactsRoutes.ContactsGraph>(
startDestination = ContactsRoutes.Contacts,
) {
composable<ContactsRoutes.Contacts> {
ContactsScreen(
uiViewModel,
onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) }
)
}
composable<ContactsRoutes.Messages>(
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/messages/{contactKey}?message={message}"
action = "android.intent.action.VIEW"
},
)
) { backStackEntry ->
val args = backStackEntry.toRoute<ContactsRoutes.Messages>()
MessageScreen(
contactKey = args.contactKey,
message = args.message,
viewModel = uiViewModel,
navigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetail(it)) },
onNavigateBack = navController::navigateUp,
)
}
}
composable<ContactsRoutes.Share>(
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/share?message={message}"
action = "android.intent.action.VIEW"
}
)
) { backStackEntry ->
val message = backStackEntry.toRoute<ContactsRoutes.Share>().message
ShareScreen(uiViewModel) {
navController.navigate(ContactsRoutes.Messages(it, message)) {
popUpTo<ContactsRoutes.Share> { inclusive = true }
}
}
}
composable<ContactsRoutes.QuickChat> {
QuickChatScreen()
}
}

View file

@ -27,17 +27,11 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navDeepLink
import androidx.navigation.toRoute
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.contact.ContactsScreen
import com.geeksville.mesh.ui.debug.DebugScreen
import com.geeksville.mesh.ui.map.MapView
import com.geeksville.mesh.ui.message.MessageScreen
import com.geeksville.mesh.ui.message.QuickChatScreen
import com.geeksville.mesh.ui.sharing.ShareScreen
import kotlinx.serialization.Serializable
enum class AdminRoute(@StringRes val title: Int) {
@ -50,129 +44,15 @@ enum class AdminRoute(@StringRes val title: Int) {
const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic"
@Serializable
sealed interface Graph : Route {
@Serializable
data class RadioConfigGraph(val destNum: Int? = null) : Graph
}
sealed interface Graph : Route
@Serializable
sealed interface Route {
@Serializable
data object Contacts : Route
@Serializable
data object Map : Route
@Serializable
data object DebugPanel : Route
@Serializable
data class Messages(val contactKey: String, val message: String = "") : Route
@Serializable
data object QuickChat : Route
@Serializable
data class Share(val message: String) : Route
@Serializable
data class RadioConfig(val destNum: Int? = null) : Route
@Serializable
data object User : Route
@Serializable
data object ChannelConfig : Route
@Serializable
data object Device : Route
@Serializable
data object Position : Route
@Serializable
data object Power : Route
@Serializable
data object Network : Route
@Serializable
data object Display : Route
@Serializable
data object LoRa : Route
@Serializable
data object Bluetooth : Route
@Serializable
data object Security : Route
@Serializable
data object MQTT : Route
@Serializable
data object Serial : Route
@Serializable
data object ExtNotification : Route
@Serializable
data object StoreForward : Route
@Serializable
data object RangeTest : Route
@Serializable
data object Telemetry : Route
@Serializable
data object CannedMessage : Route
@Serializable
data object Audio : Route
@Serializable
data object RemoteHardware : Route
@Serializable
data object NeighborInfo : Route
@Serializable
data object AmbientLighting : Route
@Serializable
data object DetectionSensor : Route
@Serializable
data object Paxcounter : Route
@Serializable
data class NodeDetail(val destNum: Int? = null) : Route
@Serializable
data object DeviceMetrics : Route
@Serializable
data object NodeMap : Route
@Serializable
data object PositionLog : Route
@Serializable
data object EnvironmentMetrics : Route
@Serializable
data object SignalMetrics : Route
@Serializable
data object PowerMetrics : Route
@Serializable
data object TracerouteLog : Route
@Serializable
data object HostMetricsLog : Route
}
fun NavDestination.isConfigRoute(): Boolean {
@ -187,8 +67,8 @@ fun NavDestination.isNodeDetailRoute(): Boolean {
fun NavDestination.showLongNameTitle(): Boolean {
return !this.isTopLevel() && (
this.hasRoute<Route.RadioConfig>() ||
this.hasRoute<Route.NodeDetail>() ||
this.hasRoute<RadioConfigRoutes.RadioConfig>() ||
this.hasRoute<NodesRoutes.NodeDetail>() ||
this.isConfigRoute() ||
this.isNodeDetailRoute()
)
@ -203,70 +83,19 @@ fun NavGraph(
) {
NavHost(
navController = navController,
startDestination = if (uIViewModel.bondedAddress.isNullOrBlank()) {
startDestination = if (uIViewModel.isConnected()) {
ConnectionsRoutes.ConnectionsGraph
} else {
Route.Contacts
ContactsRoutes.ContactsGraph
},
modifier = modifier,
) {
composable<Route.Contacts> {
ContactsScreen(
uIViewModel,
onNavigate = { navController.navigate(Route.Messages(it)) }
)
}
composable<Route.Map> {
MapView(uIViewModel)
}
contactsGraph(navController, uIViewModel)
nodesGraph(navController, uIViewModel,)
composable<Route.Map> { MapView(uIViewModel) }
channelsGraph(navController, uIViewModel)
connectionsGraph(navController, uIViewModel)
composable<Route.DebugPanel> {
DebugScreen()
}
composable<Route.Messages>(
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/messages/{contactKey}?message={message}"
action = "android.intent.action.VIEW"
},
)
) { backStackEntry ->
val args = backStackEntry.toRoute<Route.Messages>()
MessageScreen(
contactKey = args.contactKey,
message = args.message,
viewModel = uIViewModel,
navigateToMessages = { navController.navigate(Route.Messages(it)) },
navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
onNavigateBack = navController::navigateUp,
)
}
composable<Route.QuickChat> {
QuickChatScreen()
}
nodesGraph(
navController,
uIViewModel,
)
composable<Route.DebugPanel> { DebugScreen() }
radioConfigGraph(navController, uIViewModel)
composable<Route.Share>(
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/share?message={message}"
action = "android.intent.action.VIEW"
}
)
) { backStackEntry ->
val message = backStackEntry.toRoute<Route.Share>().message
ShareScreen(uIViewModel) {
navController.navigate(Route.Messages(it, message)) {
popUpTo<Route.Share> { inclusive = true }
}
}
}
}
}

View file

@ -56,6 +56,36 @@ sealed class NodesRoutes {
@Serializable
data object NodeDetailGraph : Graph
@Serializable
data class NodeDetail(val destNum: Int? = null) : Route
}
sealed class NodeDetailRoutes {
@Serializable
data object DeviceMetrics : Route
@Serializable
data object NodeMap : Route
@Serializable
data object PositionLog : Route
@Serializable
data object EnvironmentMetrics : Route
@Serializable
data object SignalMetrics : Route
@Serializable
data object PowerMetrics : Route
@Serializable
data object TracerouteLog : Route
@Serializable
data object HostMetricsLog : Route
}
fun NavGraphBuilder.nodesGraph(
@ -69,10 +99,10 @@ fun NavGraphBuilder.nodesGraph(
NodeScreen(
model = uiViewModel,
navigateToMessages = {
navController.navigate(Route.Messages(it))
navController.navigate(ContactsRoutes.Messages(it))
},
navigateToNodeDetails = {
navController.navigate(Route.NodeDetail(it))
navController.navigate(NodesRoutes.NodeDetail(it))
},
)
}
@ -85,16 +115,16 @@ fun NavGraphBuilder.nodeDetailGraph(
uiViewModel: UIViewModel,
) {
navigation<NodesRoutes.NodeDetailGraph>(
startDestination = Route.NodeDetail()
startDestination = NodesRoutes.NodeDetail()
) {
composable<Route.NodeDetail> { backStackEntry ->
composable<NodesRoutes.NodeDetail> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<NodesRoutes.NodeDetailGraph>()
}
NodeDetailScreen(
uiViewModel = uiViewModel,
navigateToMessages = {
navController.navigate(Route.Messages(it))
navController.navigate(ContactsRoutes.Messages(it))
},
onNavigate = {
navController.navigate(it)
@ -137,12 +167,12 @@ enum class NodeDetailRoute(
val route: Route,
val icon: ImageVector?,
) {
DEVICE(R.string.device, Route.DeviceMetrics, Icons.Default.Router),
NODE_MAP(R.string.node_map, Route.NodeMap, Icons.Default.LocationOn),
POSITION_LOG(R.string.position_log, Route.PositionLog, Icons.Default.LocationOn),
ENVIRONMENT(R.string.environment, Route.EnvironmentMetrics, Icons.Default.LightMode),
SIGNAL(R.string.signal, Route.SignalMetrics, Icons.Default.CellTower),
TRACEROUTE(R.string.traceroute, Route.TracerouteLog, Icons.Default.PermScanWifi),
POWER(R.string.power, Route.PowerMetrics, Icons.Default.Power),
HOST(R.string.host, Route.HostMetricsLog, Icons.Default.Memory),
DEVICE(R.string.device, NodeDetailRoutes.DeviceMetrics, Icons.Default.Router),
NODE_MAP(R.string.node_map, NodeDetailRoutes.NodeMap, Icons.Default.LocationOn),
POSITION_LOG(R.string.position_log, NodeDetailRoutes.PositionLog, Icons.Default.LocationOn),
ENVIRONMENT(R.string.environment, NodeDetailRoutes.EnvironmentMetrics, Icons.Default.LightMode),
SIGNAL(R.string.signal, NodeDetailRoutes.SignalMetrics, Icons.Default.CellTower),
TRACEROUTE(R.string.traceroute, NodeDetailRoutes.TracerouteLog, Icons.Default.PermScanWifi),
POWER(R.string.power, NodeDetailRoutes.PowerMetrics, Icons.Default.Power),
HOST(R.string.host, NodeDetailRoutes.HostMetricsLog, Icons.Default.Memory),
}

View file

@ -76,6 +76,83 @@ import com.geeksville.mesh.ui.radioconfig.components.SerialConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.StoreForwardConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen
import kotlinx.serialization.Serializable
sealed class RadioConfigRoutes {
@Serializable
data class RadioConfigGraph(val destNum: Int? = null) : Graph
@Serializable
data class RadioConfig(val destNum: Int? = null) : Route
@Serializable
data object User : Route
@Serializable
data object ChannelConfig : Route
@Serializable
data object Device : Route
@Serializable
data object Position : Route
@Serializable
data object Power : Route
@Serializable
data object Network : Route
@Serializable
data object Display : Route
@Serializable
data object LoRa : Route
@Serializable
data object Bluetooth : Route
@Serializable
data object Security : Route
@Serializable
data object MQTT : Route
@Serializable
data object Serial : Route
@Serializable
data object ExtNotification : Route
@Serializable
data object StoreForward : Route
@Serializable
data object RangeTest : Route
@Serializable
data object Telemetry : Route
@Serializable
data object CannedMessage : Route
@Serializable
data object Audio : Route
@Serializable
data object RemoteHardware : Route
@Serializable
data object NeighborInfo : Route
@Serializable
data object AmbientLighting : Route
@Serializable
data object DetectionSensor : Route
@Serializable
data object Paxcounter : Route
}
fun getNavRouteFrom(routeName: String): Route? {
return ConfigRoute.entries.find { it.name == routeName }?.route
@ -83,19 +160,19 @@ fun getNavRouteFrom(routeName: String): Route? {
}
fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation<Graph.RadioConfigGraph>(
startDestination = Route.RadioConfig(),
navigation<RadioConfigRoutes.RadioConfigGraph>(
startDestination = RadioConfigRoutes.RadioConfig(),
) {
composable<Route.RadioConfig> { backStackEntry ->
composable<RadioConfigRoutes.RadioConfig> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.RadioConfigGraph>()
navController.getBackStackEntry<RadioConfigRoutes.RadioConfigGraph>()
}
RadioConfigScreen(
uiViewModel = uiViewModel,
viewModel = hiltViewModel(parentEntry)
) {
navController.navigate(it) {
popUpTo(Route.RadioConfig()) {
popUpTo(RadioConfigRoutes.RadioConfig()) {
inclusive = false
}
}
@ -112,7 +189,7 @@ private fun NavGraphBuilder.configRoutes(
ConfigRoute.entries.forEach { configRoute ->
composable(configRoute.route::class) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.RadioConfigGraph>()
navController.getBackStackEntry<RadioConfigRoutes.RadioConfigGraph>()
}
when (configRoute) {
ConfigRoute.USER -> UserConfigScreen(hiltViewModel(parentEntry))
@ -137,7 +214,7 @@ private fun NavGraphBuilder.moduleRoutes(
ModuleRoute.entries.forEach { moduleRoute ->
composable(moduleRoute.route::class) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.RadioConfigGraph>()
navController.getBackStackEntry<RadioConfigRoutes.RadioConfigGraph>()
}
when (moduleRoute) {
ModuleRoute.MQTT -> MQTTConfigScreen(hiltViewModel(parentEntry))
@ -181,16 +258,16 @@ enum class ConfigRoute(
val icon: ImageVector?,
val type: Int = 0
) {
USER(R.string.user, Route.User, Icons.Default.Person, 0),
CHANNELS(R.string.channels, Route.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
DEVICE(R.string.device, Route.Device, Icons.Default.Router, 0),
POSITION(R.string.position, Route.Position, Icons.Default.LocationOn, 1),
POWER(R.string.power, Route.Power, Icons.Default.Power, 2),
NETWORK(R.string.network, Route.Network, Icons.Default.Wifi, 3),
DISPLAY(R.string.display, Route.Display, Icons.Default.DisplaySettings, 4),
LORA(R.string.lora, Route.LoRa, Icons.Default.CellTower, 5),
BLUETOOTH(R.string.bluetooth, Route.Bluetooth, Icons.Default.Bluetooth, 6),
SECURITY(R.string.security, Route.Security, Icons.Default.Security, 7),
USER(R.string.user, RadioConfigRoutes.User, Icons.Default.Person, 0),
CHANNELS(R.string.channels, RadioConfigRoutes.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
DEVICE(R.string.device, RadioConfigRoutes.Device, Icons.Default.Router, 0),
POSITION(R.string.position, RadioConfigRoutes.Position, Icons.Default.LocationOn, 1),
POWER(R.string.power, RadioConfigRoutes.Power, Icons.Default.Power, 2),
NETWORK(R.string.network, RadioConfigRoutes.Network, Icons.Default.Wifi, 3),
DISPLAY(R.string.display, RadioConfigRoutes.Display, Icons.Default.DisplaySettings, 4),
LORA(R.string.lora, RadioConfigRoutes.LoRa, Icons.Default.CellTower, 5),
BLUETOOTH(R.string.bluetooth, RadioConfigRoutes.Bluetooth, Icons.Default.Bluetooth, 6),
SECURITY(R.string.security, RadioConfigRoutes.Security, Icons.Default.Security, 7),
;
companion object {
@ -213,39 +290,39 @@ enum class ModuleRoute(
val icon: ImageVector?,
val type: Int = 0
) {
MQTT(R.string.mqtt, Route.MQTT, Icons.Default.Cloud, 0),
SERIAL(R.string.serial, Route.Serial, Icons.Default.Usb, 1),
MQTT(R.string.mqtt, RadioConfigRoutes.MQTT, Icons.Default.Cloud, 0),
SERIAL(R.string.serial, RadioConfigRoutes.Serial, Icons.Default.Usb, 1),
EXT_NOTIFICATION(
R.string.external_notification,
Route.ExtNotification,
RadioConfigRoutes.ExtNotification,
Icons.Default.Notifications,
2
),
STORE_FORWARD(
R.string.store_forward,
Route.StoreForward,
RadioConfigRoutes.StoreForward,
Icons.AutoMirrored.Default.Forward,
3
),
RANGE_TEST(R.string.range_test, Route.RangeTest, Icons.Default.Speed, 4),
TELEMETRY(R.string.telemetry, Route.Telemetry, Icons.Default.DataUsage, 5),
RANGE_TEST(R.string.range_test, RadioConfigRoutes.RangeTest, Icons.Default.Speed, 4),
TELEMETRY(R.string.telemetry, RadioConfigRoutes.Telemetry, Icons.Default.DataUsage, 5),
CANNED_MESSAGE(
R.string.canned_message,
Route.CannedMessage,
RadioConfigRoutes.CannedMessage,
Icons.AutoMirrored.Default.Message,
6
),
AUDIO(R.string.audio, Route.Audio, Icons.AutoMirrored.Default.VolumeUp, 7),
AUDIO(R.string.audio, RadioConfigRoutes.Audio, Icons.AutoMirrored.Default.VolumeUp, 7),
REMOTE_HARDWARE(
R.string.remote_hardware,
Route.RemoteHardware,
RadioConfigRoutes.RemoteHardware,
Icons.Default.SettingsRemote,
8
),
NEIGHBOR_INFO(R.string.neighbor_info, Route.NeighborInfo, Icons.Default.People, 9),
AMBIENT_LIGHTING(R.string.ambient_lighting, Route.AmbientLighting, Icons.Default.LightMode, 10),
DETECTION_SENSOR(R.string.detection_sensor, Route.DetectionSensor, Icons.Default.Sensors, 11),
PAXCOUNTER(R.string.paxcounter, Route.Paxcounter, Icons.Default.PermScanWifi, 12),
NEIGHBOR_INFO(R.string.neighbor_info, RadioConfigRoutes.NeighborInfo, Icons.Default.People, 9),
AMBIENT_LIGHTING(R.string.ambient_lighting, RadioConfigRoutes.AmbientLighting, Icons.Default.LightMode, 10),
DETECTION_SENSOR(R.string.detection_sensor, RadioConfigRoutes.DetectionSensor, Icons.Default.Sensors, 11),
PAXCOUNTER(R.string.paxcounter, RadioConfigRoutes.Paxcounter, Icons.Default.PermScanWifi, 12),
;
val bitfield: Int get() = 1 shl ordinal