From 26f210047df0817aeef51269379858ab9e8a9a69 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 2 Nov 2024 09:34:30 -0300 Subject: [PATCH] refactor: split `MetricsViewModel` state updates - Consolidates `MetricViewModel` back to a single state flow - Introduces a `MutableStateFlow` for state updates, allowing more independent control - Moves `Telemetry`, `MeshPacket`, and config updates into separate coroutines --- .../geeksville/mesh/model/MetricsViewModel.kt | 121 ++++++++++-------- .../java/com/geeksville/mesh/ui/NodeDetail.kt | 4 +- .../mesh/ui/components/EnvironmentMetrics.kt | 4 +- .../mesh/ui/components/TracerouteLog.kt | 8 +- 4 files changed, 75 insertions(+), 62 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 7a7b0af8b..6785379cc 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt @@ -6,51 +6,47 @@ import com.geeksville.mesh.CoroutineDispatchers import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.Portnums.PortNum import com.geeksville.mesh.TelemetryProtos.Telemetry +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 dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject data class MetricsState( val isManaged: Boolean = true, + val isFahrenheit: Boolean = false, val deviceMetrics: List = emptyList(), val environmentMetrics: List = emptyList(), val signalMetrics: List = emptyList(), - val hasTracerouteLogs: Boolean = false, - val environmentDisplayFahrenheit: Boolean = false, + val tracerouteRequests: List = emptyList(), + val tracerouteResults: List = emptyList(), ) { fun hasDeviceMetrics() = deviceMetrics.isNotEmpty() fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty() fun hasSignalMetrics() = signalMetrics.isNotEmpty() + fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty() companion object { val Empty = MetricsState() } } -data class TracerouteLogState( - val requests: List = emptyList(), - val results: List = emptyList(), -) { - companion object { - val Empty = TracerouteLogState() - } -} - @HiltViewModel class MetricsViewModel @Inject constructor( private val dispatchers: CoroutineDispatchers, private val meshLogRepository: MeshLogRepository, private val radioConfigRepository: RadioConfigRepository, -) : ViewModel() { +) : ViewModel(), Logging { private val destNum = MutableStateFlow(0) private fun MeshPacket.hasValidSignal(): Boolean = @@ -66,48 +62,65 @@ class MetricsViewModel @Inject constructor( meshLogRepository.deleteLog(uuid) } - @OptIn(ExperimentalCoroutinesApi::class) - val tracerouteState = destNum.flatMapLatest { destNum -> - combine( - meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE), - meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE), - ) { request, response -> - TracerouteLogState( - requests = request.filter { it.hasValidTraceroute() }, - results = response, - ) - } - }.stateIn( - scope = viewModelScope, - started = WhileSubscribed(stopTimeoutMillis = 5000L), - initialValue = TracerouteLogState.Empty, - ) + private val _state = MutableStateFlow(MetricsState.Empty) + val state: StateFlow = _state - @OptIn(ExperimentalCoroutinesApi::class) - val state = destNum.flatMapLatest { destNum -> - combine( - meshLogRepository.getTelemetryFrom(destNum), - meshLogRepository.getMeshPacketsFrom(destNum), - meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE), - radioConfigRepository.deviceProfileFlow, - ) { telemetry, meshPackets, traceroute, profile -> + init { + radioConfigRepository.deviceProfileFlow.onEach { profile -> val moduleConfig = profile.moduleConfig - MetricsState( - isManaged = profile.config.security.isManaged, - deviceMetrics = telemetry.filter { it.hasDeviceMetrics() }, - environmentMetrics = telemetry.filter { - it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f - }, - signalMetrics = meshPackets.filter { it.hasValidSignal() }, - hasTracerouteLogs = traceroute.any { it.hasValidTraceroute() }, - environmentDisplayFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit, - ) - } - }.stateIn( - scope = viewModelScope, - started = WhileSubscribed(stopTimeoutMillis = 5000L), - initialValue = MetricsState.Empty, - ) + _state.update { state -> + state.copy( + isManaged = profile.config.security.isManaged, + isFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit, + ) + } + }.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 + }, + ) + } + } + }.launchIn(viewModelScope) + + @OptIn(ExperimentalCoroutinesApi::class) + destNum.flatMapLatest { destNum -> + 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, + ) + } + } + }.launchIn(viewModelScope) + + debug("MetricsViewModel created") + } + + override fun onCleared() { + super.onCleared() + debug("MetricsViewModel cleared") + } /** * Used to set the Node for which the user will see charts for. 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 2abb9db04..a8c5a7cbd 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt @@ -120,7 +120,7 @@ private fun NodeDetailList( if (node.hasEnvironmentMetrics) { item { PreferenceCategory("Environment") - EnvironmentMetrics(node, metricsState.environmentDisplayFahrenheit) + EnvironmentMetrics(node, metricsState.isFahrenheit) Spacer(modifier = Modifier.height(8.dp)) } } @@ -161,7 +161,7 @@ private fun NodeDetailList( NavCard( title = stringResource(R.string.traceroute_logs), icon = Icons.Default.Route, - enabled = metricsState.hasTracerouteLogs + enabled = metricsState.hasTracerouteLogs() ) { onNavigate("TracerouteList") } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt index 3204f9446..f9d706728 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt @@ -83,7 +83,7 @@ fun EnvironmentMetricsScreen( return (celsius * 1.8F) + 32 } - val processedTelemetries: List = if (state.environmentDisplayFahrenheit) { + val processedTelemetries: List = if (state.isFahrenheit) { state.environmentMetrics.map { telemetry -> val temperatureFahrenheit = celsiusToFahrenheit(telemetry.environmentMetrics.temperature) @@ -124,7 +124,7 @@ fun EnvironmentMetricsScreen( items(processedTelemetries) { telemetry -> EnvironmentMetricsCard( telemetry, - state.environmentDisplayFahrenheit + state.isFahrenheit ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/TracerouteLog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/TracerouteLog.kt index ab6da2da1..d5c2bd05d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/TracerouteLog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/TracerouteLog.kt @@ -53,7 +53,7 @@ fun TracerouteLogScreen( viewModel: MetricsViewModel = hiltViewModel(), modifier: Modifier = Modifier, ) { - val state by viewModel.tracerouteState.collectAsStateWithLifecycle() + val state by viewModel.state.collectAsStateWithLifecycle() val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) } @@ -80,9 +80,9 @@ fun TracerouteLogScreen( modifier = modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp), ) { - items(state.requests, key = { it.uuid }) { log -> - val result = remember(state.requests) { - state.results.find { it.decoded.requestId == log.fromRadio.packet.id } + items(state.tracerouteRequests, key = { it.uuid }) { log -> + val result = remember(state.tracerouteRequests) { + state.tracerouteResults.find { it.decoded.requestId == log.fromRadio.packet.id } } val route = remember(result) { result?.fullRouteDiscovery }