From 1ae65ef2678efe6e43032723e1539ac5158b5d03 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 9 Nov 2024 06:46:47 -0300 Subject: [PATCH] refactor: pass nav args to ViewModel using `SavedStateHandle` --- .../geeksville/mesh/model/MetricsViewModel.kt | 77 +++++++------------ .../mesh/model/RadioConfigViewModel.kt | 20 +++-- .../java/com/geeksville/mesh/ui/NavGraph.kt | 1 - .../java/com/geeksville/mesh/ui/NodeDetail.kt | 4 - 4 files changed, 38 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt index 3dceafae1..6a7dba7e8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt @@ -2,8 +2,10 @@ package com.geeksville.mesh.model import android.app.Application import android.net.Uri +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits import com.geeksville.mesh.CoroutineDispatchers import com.geeksville.mesh.MeshProtos.MeshPacket @@ -14,12 +16,11 @@ import com.geeksville.mesh.android.Logging import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.entity.MeshLog import com.geeksville.mesh.repository.datastore.RadioConfigRepository +import com.geeksville.mesh.ui.Route import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update @@ -65,15 +66,16 @@ private fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) { @HiltViewModel class MetricsViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, private val app: Application, private val dispatchers: CoroutineDispatchers, private val meshLogRepository: MeshLogRepository, private val radioConfigRepository: RadioConfigRepository, ) : ViewModel(), Logging { - private val destNum = MutableStateFlow(0) + private val destNum = savedStateHandle.toRoute().destNum private fun MeshLog.hasValidTraceroute(): Boolean = with(fromRadio.packet) { - hasDecoded() && decoded.wantResponse && from == 0 && to == destNum.value + hasDecoded() && decoded.wantResponse && from == 0 && to == destNum } fun getUser(nodeNum: Int) = radioConfigRepository.getUser(nodeNum) @@ -83,7 +85,7 @@ class MetricsViewModel @Inject constructor( } fun clearPosition() = viewModelScope.launch(dispatchers.io) { - meshLogRepository.deleteLogs(destNum.value, PortNum.POSITION_APP_VALUE) + meshLogRepository.deleteLogs(destNum, PortNum.POSITION_APP_VALUE) } private val _state = MutableStateFlow(MetricsState.Empty) @@ -100,51 +102,37 @@ class MetricsViewModel @Inject constructor( } }.launchIn(viewModelScope) - @OptIn(ExperimentalCoroutinesApi::class) - destNum.flatMapLatest { destNum -> - meshLogRepository.getTelemetryFrom(destNum).onEach { telemetry -> - _state.update { state -> - state.copy( - deviceMetrics = telemetry.filter { it.hasDeviceMetrics() }, - environmentMetrics = telemetry.filter { - it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f - }, - ) - } + meshLogRepository.getTelemetryFrom(destNum).onEach { telemetry -> + _state.update { state -> + state.copy( + deviceMetrics = telemetry.filter { it.hasDeviceMetrics() }, + environmentMetrics = telemetry.filter { + it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f + }, + ) } }.launchIn(viewModelScope) - @OptIn(ExperimentalCoroutinesApi::class) - destNum.flatMapLatest { destNum -> - meshLogRepository.getMeshPacketsFrom(destNum).onEach { meshPackets -> - _state.update { state -> - state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() }) - } + meshLogRepository.getMeshPacketsFrom(destNum).onEach { meshPackets -> + _state.update { state -> + state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() }) } }.launchIn(viewModelScope) - @OptIn(ExperimentalCoroutinesApi::class) - destNum.flatMapLatest { destNum -> - combine( - meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE), - meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE), - ) { request, response -> - _state.update { state -> - state.copy( - tracerouteRequests = request.filter { it.hasValidTraceroute() }, - tracerouteResults = response, - ) - } + combine( + meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE), + meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE), + ) { request, response -> + _state.update { state -> + state.copy( + tracerouteRequests = request.filter { it.hasValidTraceroute() }, + tracerouteResults = response, + ) } }.launchIn(viewModelScope) - @OptIn(ExperimentalCoroutinesApi::class) - destNum.flatMapLatest { destNum -> - meshLogRepository.getMeshPacketsFrom(destNum, PortNum.POSITION_APP_VALUE).onEach { packets -> - _state.update { state -> - state.copy(positionLogs = packets.mapNotNull { it.toPosition() }) - } - } + meshLogRepository.getMeshPacketsFrom(destNum, PortNum.POSITION_APP_VALUE).onEach { packets -> + _state.update { state -> state.copy(positionLogs = packets.mapNotNull { it.toPosition() }) } }.launchIn(viewModelScope) debug("MetricsViewModel created") @@ -155,13 +143,6 @@ class MetricsViewModel @Inject constructor( debug("MetricsViewModel cleared") } - /** - * Used to set the Node for which the user will see charts for. - */ - fun setSelectedNode(nodeNum: Int) { - destNum.value = nodeNum - } - /** * Write the persisted Position data out to a CSV file in the specified location. */ 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 18be35a0e..70bca26e3 100644 --- a/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt @@ -3,8 +3,10 @@ package com.geeksville.mesh.model import android.app.Application import android.net.Uri import android.os.RemoteException +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import com.geeksville.mesh.AdminProtos import com.geeksville.mesh.ChannelProtos import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile @@ -27,14 +29,17 @@ import com.geeksville.mesh.ui.AdminRoute import com.geeksville.mesh.ui.ConfigRoute import com.geeksville.mesh.ui.ModuleRoute import com.geeksville.mesh.ui.ResponseState +import com.geeksville.mesh.ui.Route import com.google.protobuf.MessageLite import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -62,24 +67,16 @@ data class RadioConfigState( @HiltViewModel class RadioConfigViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, private val app: Application, private val radioConfigRepository: RadioConfigRepository, ) : ViewModel(), Logging { - private val meshService: IMeshService? get() = radioConfigRepository.meshService - private val _destNum = MutableStateFlow(null) + private val destNum = savedStateHandle.toRoute().destNum private val _destNode = MutableStateFlow(null) val destNode: StateFlow get() = _destNode - /** - * Sets the destination [NodeEntity] used in Radio Configuration. - * @param num Destination nodeNum (or null for our local [NodeEntity]). - */ - fun setDestNum(num: Int?) { - _destNum.value = num - } - private val requestIds = MutableStateFlow(hashSetOf()) private val _radioConfigState = MutableStateFlow(RadioConfigState()) val radioConfigState: StateFlow = _radioConfigState @@ -88,7 +85,8 @@ class RadioConfigViewModel @Inject constructor( val currentDeviceProfile get() = _currentDeviceProfile.value init { - combine(_destNum, radioConfigRepository.nodeDBbyNum) { destNum, nodes -> + @OptIn(ExperimentalCoroutinesApi::class) + radioConfigRepository.nodeDBbyNum.mapLatest { nodes -> nodes[destNum] ?: nodes.values.firstOrNull() }.onEach { _destNode.value = it }.launchIn(viewModelScope) 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 9eaec2865..38994f8a9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt @@ -120,7 +120,6 @@ class NavGraphFragment : ScreenFragment("NavGraph"), Logging { "NodeDetails" -> Route.NodeDetail(destNum!!) else -> Route.RadioConfig(destNum) } - model.setDestNum(destNum) return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) 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 bcbf0bd29..bd01aff4b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt @@ -47,7 +47,6 @@ import androidx.compose.material.icons.filled.Thermostat import androidx.compose.material.icons.filled.WaterDrop import androidx.compose.material.icons.filled.Work import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -82,9 +81,6 @@ fun NodeDetailScreen( val state by viewModel.state.collectAsStateWithLifecycle() if (node != null) { - LaunchedEffect(node.num) { - viewModel.setSelectedNode(node.num) - } NodeDetailList( node = node, metricsState = state,