From 296f1944b75980cf1642fdca47875108d7539608 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 9 Nov 2024 06:23:40 -0300 Subject: [PATCH] refactor: migrate Compose navigation to type-safe args --- .../mesh/model/RadioConfigViewModel.kt | 8 +- .../java/com/geeksville/mesh/ui/NavGraph.kt | 174 +++++++++++------- .../java/com/geeksville/mesh/ui/NodeDetail.kt | 20 +- .../geeksville/mesh/ui/RadioConfigScreen.kt | 11 +- 4 files changed, 129 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt index 90e57bed7..18be35a0e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt @@ -427,7 +427,7 @@ class RadioConfigViewModel @Inject constructor( ConfigRoute.CHANNELS -> { getChannel(destNum, 0) - getConfig(destNum, ConfigRoute.LORA.configType) + getConfig(destNum, ConfigRoute.LORA.type) // channel editor is synchronous, so we don't use requestIds as total setResponseStateTotal(maxChannels + 1) } @@ -438,17 +438,17 @@ class RadioConfigViewModel @Inject constructor( if (route == ConfigRoute.LORA) { getChannel(destNum, 0) } - getConfig(destNum, route.configType) + getConfig(destNum, route.type) } is ModuleRoute -> { if (route == ModuleRoute.CANNED_MESSAGE) { getCannedMessages(destNum) } - if (route == ModuleRoute.EXTERNAL_NOTIFICATION) { + if (route == ModuleRoute.EXT_NOTIFICATION) { getRingtone(destNum) } - getModuleConfig(destNum, route.configType) + getModuleConfig(destNum, route.type) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt b/app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt index 7a16673fc..9eaec2865 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt @@ -90,6 +90,7 @@ import com.geeksville.mesh.ui.components.config.TelemetryConfigScreen import com.geeksville.mesh.ui.components.config.UserConfigScreen import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import dagger.hilt.android.AndroidEntryPoint +import kotlinx.serialization.Serializable internal fun FragmentManager.navigateToNavGraph( destNum: Int? = null, @@ -115,7 +116,10 @@ class NavGraphFragment : ScreenFragment("NavGraph"), Logging { savedInstanceState: Bundle? ): View { val destNum = arguments?.getInt("destNum") - val startDestination = arguments?.getString("startDestination") ?: "RadioConfig" + val startDestination: Any = when (arguments?.getString("startDestination")) { + "NodeDetails" -> Route.NodeDetail(destNum!!) + else -> Route.RadioConfig(destNum) + } model.setDestNum(destNum) return ComposeView(requireContext()).apply { @@ -163,35 +167,73 @@ enum class AdminRoute(@StringRes val title: Int) { NODEDB_RESET(R.string.nodedb_reset), } -// Config (configType = AdminProtos.AdminMessage.ConfigType) -enum class ConfigRoute(val title: String, val icon: ImageVector?, val configType: Int = 0) { - USER("User", Icons.Default.Person, 0), - CHANNELS("Channels", Icons.AutoMirrored.Default.List, 0), - DEVICE("Device", Icons.Default.Router, 0), - POSITION("Position", Icons.Default.LocationOn, 1), - POWER("Power", Icons.Default.Power, 2), - NETWORK("Network", Icons.Default.Wifi, 3), - DISPLAY("Display", Icons.Default.DisplaySettings, 4), - LORA("LoRa", Icons.Default.CellTower, 5), - BLUETOOTH("Bluetooth", Icons.Default.Bluetooth, 6), - SECURITY("Security", Icons.Default.Security, configType = 7), +sealed interface Route { + @Serializable + data class RadioConfig(val destNum: Int? = null) : Route + @Serializable data object User : Route + @Serializable data object Channels : 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) : 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 TracerouteLog : Route } -// ModuleConfig (configType = AdminProtos.AdminMessage.ModuleConfigType) -enum class ModuleRoute(val title: String, val icon: ImageVector?, val configType: Int = 0) { - MQTT("MQTT", Icons.Default.Cloud, 0), - SERIAL("Serial", Icons.Default.Usb, 1), - EXTERNAL_NOTIFICATION("External Notification", Icons.Default.Notifications, 2), - STORE_FORWARD("Store & Forward", Icons.AutoMirrored.Default.Forward, 3), - RANGE_TEST("Range Test", Icons.Default.Speed, 4), - TELEMETRY("Telemetry", Icons.Default.DataUsage, 5), - CANNED_MESSAGE("Canned Message", Icons.AutoMirrored.Default.Message, 6), - AUDIO("Audio", Icons.AutoMirrored.Default.VolumeUp, 7), - REMOTE_HARDWARE("Remote Hardware", Icons.Default.SettingsRemote, 8), - NEIGHBOR_INFO("Neighbor Info", Icons.Default.People, 9), - AMBIENT_LIGHTING("Ambient Lighting", Icons.Default.LightMode, 10), - DETECTION_SENSOR("Detection Sensor", Icons.Default.Sensors, 11), - PAXCOUNTER("Paxcounter", Icons.Default.PermScanWifi, 12), +// Config (type = AdminProtos.AdminMessage.ConfigType) +enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) { + USER("User", Route.User, Icons.Default.Person, 0), + CHANNELS("Channels", Route.Channels, Icons.AutoMirrored.Default.List, 0), + DEVICE("Device", Route.Device, Icons.Default.Router, 0), + POSITION("Position", Route.Position, Icons.Default.LocationOn, 1), + POWER("Power", Route.Power, Icons.Default.Power, 2), + NETWORK("Network", Route.Network, Icons.Default.Wifi, 3), + DISPLAY("Display", Route.Display, Icons.Default.DisplaySettings, 4), + LORA("LoRa", Route.LoRa, Icons.Default.CellTower, 5), + BLUETOOTH("Bluetooth", Route.Bluetooth, Icons.Default.Bluetooth, 6), + SECURITY("Security", Route.Security, Icons.Default.Security, type = 7), +} + +// ModuleConfig (type = AdminProtos.AdminMessage.ModuleConfigType) +enum class ModuleRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) { + MQTT("MQTT", Route.MQTT, Icons.Default.Cloud, 0), + SERIAL("Serial", Route.Serial, Icons.Default.Usb, 1), + EXT_NOTIFICATION("External Notification", Route.ExtNotification, Icons.Default.Notifications, 2), + STORE_FORWARD("Store & Forward", Route.StoreForward, Icons.AutoMirrored.Default.Forward, 3), + RANGE_TEST("Range Test", Route.RangeTest, Icons.Default.Speed, 4), + TELEMETRY("Telemetry", Route.Telemetry, Icons.Default.DataUsage, 5), + CANNED_MESSAGE("Canned Message", Route.CannedMessage, Icons.AutoMirrored.Default.Message, 6), + AUDIO("Audio", Route.Audio, Icons.AutoMirrored.Default.VolumeUp, 7), + REMOTE_HARDWARE("Remote Hardware", Route.RemoteHardware, Icons.Default.SettingsRemote, 8), + NEIGHBOR_INFO("Neighbor Info", Route.NeighborInfo, Icons.Default.People, 9), + AMBIENT_LIGHTING("Ambient Lighting", Route.AmbientLighting, Icons.Default.LightMode, 10), + DETECTION_SENSOR("Detection Sensor", Route.DetectionSensor, Icons.Default.Sensors, 11), + PAXCOUNTER("Paxcounter", Route.Paxcounter, Icons.Default.PermScanWifi, 12), } /** @@ -235,7 +277,7 @@ fun NavGraph( node: NodeEntity?, viewModel: RadioConfigViewModel = hiltViewModel(), navController: NavHostController = rememberNavController(), - startDestination: String, + startDestination: Any, modifier: Modifier = Modifier, ) { NavHost( @@ -243,108 +285,108 @@ fun NavGraph( startDestination = startDestination, modifier = modifier, ) { - composable("NodeDetails") { + composable { NodeDetailScreen( node = node, ) { navController.navigate(route = it) } } - composable("DeviceMetrics") { - val parentEntry = remember { navController.getBackStackEntry("NodeDetails") } + composable { + val parentEntry = remember { navController.getBackStackEntry() } DeviceMetricsScreen(hiltViewModel(parentEntry)) } - composable("NodeMap") { - val parentEntry = remember { navController.getBackStackEntry("NodeDetails") } + composable { + val parentEntry = remember { navController.getBackStackEntry() } NodeMapScreen(hiltViewModel(parentEntry)) } - composable("PositionLog") { - val parentEntry = remember { navController.getBackStackEntry("NodeDetails") } + composable { + val parentEntry = remember { navController.getBackStackEntry() } PositionLogScreen(hiltViewModel(parentEntry)) } - composable("EnvironmentMetrics") { - val parentEntry = remember { navController.getBackStackEntry("NodeDetails") } + composable { + val parentEntry = remember { navController.getBackStackEntry() } EnvironmentMetricsScreen(hiltViewModel(parentEntry)) } - composable("SignalMetrics") { - val parentEntry = remember { navController.getBackStackEntry("NodeDetails") } + composable { + val parentEntry = remember { navController.getBackStackEntry() } SignalMetricsScreen(hiltViewModel(parentEntry)) } - composable("TracerouteList") { - val parentEntry = remember { navController.getBackStackEntry("NodeDetails") } + composable { + val parentEntry = remember { navController.getBackStackEntry() } TracerouteLogScreen(hiltViewModel(parentEntry)) } - composable("RadioConfig") { + composable { RadioConfigScreen( node = node, viewModel = viewModel, ) { navController.navigate(route = it) } } - composable(ConfigRoute.USER.name) { + composable { UserConfigScreen(viewModel) } - composable(ConfigRoute.CHANNELS.name) { + composable { ChannelConfigScreen(viewModel) } - composable(ConfigRoute.DEVICE.name) { + composable { DeviceConfigScreen(viewModel) } - composable(ConfigRoute.POSITION.name) { + composable { PositionConfigScreen(viewModel) } - composable(ConfigRoute.POWER.name) { + composable { PowerConfigScreen(viewModel) } - composable(ConfigRoute.NETWORK.name) { + composable { NetworkConfigScreen(viewModel) } - composable(ConfigRoute.DISPLAY.name) { + composable { DisplayConfigScreen(viewModel) } - composable(ConfigRoute.LORA.name) { + composable { LoRaConfigScreen(viewModel) } - composable(ConfigRoute.BLUETOOTH.name) { + composable { BluetoothConfigScreen(viewModel) } - composable(ConfigRoute.SECURITY.name) { + composable { SecurityConfigScreen(viewModel) } - composable(ModuleRoute.MQTT.name) { + composable { MQTTConfigScreen(viewModel) } - composable(ModuleRoute.SERIAL.name) { + composable { SerialConfigScreen(viewModel) } - composable(ModuleRoute.EXTERNAL_NOTIFICATION.name) { + composable { ExternalNotificationConfigScreen(viewModel) } - composable(ModuleRoute.STORE_FORWARD.name) { + composable { StoreForwardConfigScreen(viewModel) } - composable(ModuleRoute.RANGE_TEST.name) { + composable { RangeTestConfigScreen(viewModel) } - composable(ModuleRoute.TELEMETRY.name) { + composable { TelemetryConfigScreen(viewModel) } - composable(ModuleRoute.CANNED_MESSAGE.name) { + composable { CannedMessageConfigScreen(viewModel) } - composable(ModuleRoute.AUDIO.name) { + composable { AudioConfigScreen(viewModel) } - composable(ModuleRoute.REMOTE_HARDWARE.name) { + composable { RemoteHardwareConfigScreen(viewModel) } - composable(ModuleRoute.NEIGHBOR_INFO.name) { + composable { NeighborInfoConfigScreen(viewModel) } - composable(ModuleRoute.AMBIENT_LIGHTING.name) { + composable { AmbientLightingConfigScreen(viewModel) } - composable(ModuleRoute.DETECTION_SENSOR.name) { + composable { DetectionSensorConfigScreen(viewModel) } - composable(ModuleRoute.PAXCOUNTER.name) { + composable { PaxcounterConfigScreen(viewModel) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt index d74448628..bcbf0bd29 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt @@ -77,7 +77,7 @@ fun NodeDetailScreen( node: NodeEntity?, viewModel: MetricsViewModel = hiltViewModel(), modifier: Modifier = Modifier, - onNavigate: (String) -> Unit, + onNavigate: (Any) -> Unit, ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -106,7 +106,7 @@ private fun NodeDetailList( node: NodeEntity, metricsState: MetricsState, modifier: Modifier = Modifier, - onNavigate: (String) -> Unit = {}, + onNavigate: (Any) -> Unit = {}, ) { LazyColumn( modifier = modifier.fillMaxSize(), @@ -147,7 +147,7 @@ private fun NodeDetailList( icon = Icons.Default.Settings, enabled = true ) { - onNavigate("RadioConfig") + onNavigate(Route.RadioConfig(node.num)) } } } @@ -231,13 +231,13 @@ private fun NodeDetailsContent(node: NodeEntity) { } @Composable -fun LogNavigationList(state: MetricsState, onNavigate: (String) -> Unit) { +fun LogNavigationList(state: MetricsState, onNavigate: (Any) -> Unit) { NavCard( title = stringResource(R.string.device_metrics_log), icon = Icons.Default.ChargingStation, enabled = state.hasDeviceMetrics() ) { - onNavigate("DeviceMetrics") + onNavigate(Route.DeviceMetrics) } NavCard( @@ -245,7 +245,7 @@ fun LogNavigationList(state: MetricsState, onNavigate: (String) -> Unit) { icon = Icons.Default.Map, enabled = state.hasPositionLogs() ) { - onNavigate("NodeMap") + onNavigate(Route.NodeMap) } NavCard( @@ -253,7 +253,7 @@ fun LogNavigationList(state: MetricsState, onNavigate: (String) -> Unit) { icon = Icons.Default.LocationOn, enabled = state.hasPositionLogs() ) { - onNavigate("PositionLog") + onNavigate(Route.PositionLog) } NavCard( @@ -261,7 +261,7 @@ fun LogNavigationList(state: MetricsState, onNavigate: (String) -> Unit) { icon = Icons.Default.Thermostat, enabled = state.hasEnvironmentMetrics() ) { - onNavigate("EnvironmentMetrics") + onNavigate(Route.EnvironmentMetrics) } NavCard( @@ -269,7 +269,7 @@ fun LogNavigationList(state: MetricsState, onNavigate: (String) -> Unit) { icon = Icons.Default.SignalCellularAlt, enabled = state.hasSignalMetrics() ) { - onNavigate("SignalMetrics") + onNavigate(Route.SignalMetrics) } NavCard( @@ -277,7 +277,7 @@ fun LogNavigationList(state: MetricsState, onNavigate: (String) -> Unit) { icon = Icons.Default.Route, enabled = state.hasTracerouteLogs() ) { - onNavigate("TracerouteList") + onNavigate(Route.TracerouteLog) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/RadioConfigScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/RadioConfigScreen.kt index 442bbd01d..063e2ba3b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/RadioConfigScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/RadioConfigScreen.kt @@ -54,13 +54,18 @@ import com.geeksville.mesh.ui.components.PreferenceCategory import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog import com.geeksville.mesh.ui.components.config.PacketResponseStateDialog +private fun getNavRouteFrom(routeName: String): Any? { + return ConfigRoute.entries.find { it.name == routeName }?.route + ?: ModuleRoute.entries.find { it.name == routeName }?.route +} + @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun RadioConfigScreen( node: NodeEntity?, viewModel: RadioConfigViewModel = hiltViewModel(), modifier: Modifier = Modifier, - onNavigate: (String) -> Unit = {} + onNavigate: (Any) -> Unit = {} ) { val isLocal = node?.num == viewModel.myNodeNum val state by viewModel.radioConfigState.collectAsStateWithLifecycle() @@ -74,9 +79,7 @@ fun RadioConfigScreen( viewModel.clearPacketResponse() }, onComplete = { - val route = state.route - if (ConfigRoute.entries.any { it.name == route } || - ModuleRoute.entries.any { it.name == route }) { + getNavRouteFrom(state.route)?.let { route -> isWaiting = false viewModel.clearPacketResponse() onNavigate(route)