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
pull/1376/head
andrekir 2024-11-02 09:34:30 -03:00 zatwierdzone przez Andre K
rodzic dcd5ca1c8e
commit 26f210047d
4 zmienionych plików z 75 dodań i 62 usunięć

Wyświetl plik

@ -6,51 +6,47 @@ import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.Portnums.PortNum import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.TelemetryProtos.Telemetry import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.entity.MeshLog import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow 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.combine
import kotlinx.coroutines.flow.flatMapLatest 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 kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
data class MetricsState( data class MetricsState(
val isManaged: Boolean = true, val isManaged: Boolean = true,
val isFahrenheit: Boolean = false,
val deviceMetrics: List<Telemetry> = emptyList(), val deviceMetrics: List<Telemetry> = emptyList(),
val environmentMetrics: List<Telemetry> = emptyList(), val environmentMetrics: List<Telemetry> = emptyList(),
val signalMetrics: List<MeshPacket> = emptyList(), val signalMetrics: List<MeshPacket> = emptyList(),
val hasTracerouteLogs: Boolean = false, val tracerouteRequests: List<MeshLog> = emptyList(),
val environmentDisplayFahrenheit: Boolean = false, val tracerouteResults: List<MeshPacket> = emptyList(),
) { ) {
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty() fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty() fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
fun hasSignalMetrics() = signalMetrics.isNotEmpty() fun hasSignalMetrics() = signalMetrics.isNotEmpty()
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
companion object { companion object {
val Empty = MetricsState() val Empty = MetricsState()
} }
} }
data class TracerouteLogState(
val requests: List<MeshLog> = emptyList(),
val results: List<MeshPacket> = emptyList(),
) {
companion object {
val Empty = TracerouteLogState()
}
}
@HiltViewModel @HiltViewModel
class MetricsViewModel @Inject constructor( class MetricsViewModel @Inject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val meshLogRepository: MeshLogRepository, private val meshLogRepository: MeshLogRepository,
private val radioConfigRepository: RadioConfigRepository, private val radioConfigRepository: RadioConfigRepository,
) : ViewModel() { ) : ViewModel(), Logging {
private val destNum = MutableStateFlow(0) private val destNum = MutableStateFlow(0)
private fun MeshPacket.hasValidSignal(): Boolean = private fun MeshPacket.hasValidSignal(): Boolean =
@ -66,48 +62,65 @@ class MetricsViewModel @Inject constructor(
meshLogRepository.deleteLog(uuid) meshLogRepository.deleteLog(uuid)
} }
@OptIn(ExperimentalCoroutinesApi::class) private val _state = MutableStateFlow(MetricsState.Empty)
val tracerouteState = destNum.flatMapLatest { destNum -> val state: StateFlow<MetricsState> = _state
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,
)
@OptIn(ExperimentalCoroutinesApi::class) init {
val state = destNum.flatMapLatest { destNum -> radioConfigRepository.deviceProfileFlow.onEach { profile ->
combine(
meshLogRepository.getTelemetryFrom(destNum),
meshLogRepository.getMeshPacketsFrom(destNum),
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
radioConfigRepository.deviceProfileFlow,
) { telemetry, meshPackets, traceroute, profile ->
val moduleConfig = profile.moduleConfig val moduleConfig = profile.moduleConfig
MetricsState( _state.update { state ->
isManaged = profile.config.security.isManaged, state.copy(
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() }, isManaged = profile.config.security.isManaged,
environmentMetrics = telemetry.filter { isFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit,
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f )
}, }
signalMetrics = meshPackets.filter { it.hasValidSignal() }, }.launchIn(viewModelScope)
hasTracerouteLogs = traceroute.any { it.hasValidTraceroute() },
environmentDisplayFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit, @OptIn(ExperimentalCoroutinesApi::class)
) destNum.flatMapLatest { destNum ->
} meshLogRepository.getTelemetryFrom(destNum).onEach { telemetry ->
}.stateIn( _state.update { state ->
scope = viewModelScope, state.copy(
started = WhileSubscribed(stopTimeoutMillis = 5000L), deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
initialValue = MetricsState.Empty, 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. * Used to set the Node for which the user will see charts for.

Wyświetl plik

@ -120,7 +120,7 @@ private fun NodeDetailList(
if (node.hasEnvironmentMetrics) { if (node.hasEnvironmentMetrics) {
item { item {
PreferenceCategory("Environment") PreferenceCategory("Environment")
EnvironmentMetrics(node, metricsState.environmentDisplayFahrenheit) EnvironmentMetrics(node, metricsState.isFahrenheit)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
@ -161,7 +161,7 @@ private fun NodeDetailList(
NavCard( NavCard(
title = stringResource(R.string.traceroute_logs), title = stringResource(R.string.traceroute_logs),
icon = Icons.Default.Route, icon = Icons.Default.Route,
enabled = metricsState.hasTracerouteLogs enabled = metricsState.hasTracerouteLogs()
) { ) {
onNavigate("TracerouteList") onNavigate("TracerouteList")
} }

Wyświetl plik

@ -83,7 +83,7 @@ fun EnvironmentMetricsScreen(
return (celsius * 1.8F) + 32 return (celsius * 1.8F) + 32
} }
val processedTelemetries: List<Telemetry> = if (state.environmentDisplayFahrenheit) { val processedTelemetries: List<Telemetry> = if (state.isFahrenheit) {
state.environmentMetrics.map { telemetry -> state.environmentMetrics.map { telemetry ->
val temperatureFahrenheit = val temperatureFahrenheit =
celsiusToFahrenheit(telemetry.environmentMetrics.temperature) celsiusToFahrenheit(telemetry.environmentMetrics.temperature)
@ -124,7 +124,7 @@ fun EnvironmentMetricsScreen(
items(processedTelemetries) { telemetry -> items(processedTelemetries) { telemetry ->
EnvironmentMetricsCard( EnvironmentMetricsCard(
telemetry, telemetry,
state.environmentDisplayFahrenheit state.isFahrenheit
) )
} }
} }

Wyświetl plik

@ -53,7 +53,7 @@ fun TracerouteLogScreen(
viewModel: MetricsViewModel = hiltViewModel(), viewModel: MetricsViewModel = hiltViewModel(),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val state by viewModel.tracerouteState.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
val dateFormat = remember { val dateFormat = remember {
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
} }
@ -80,9 +80,9 @@ fun TracerouteLogScreen(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
) { ) {
items(state.requests, key = { it.uuid }) { log -> items(state.tracerouteRequests, key = { it.uuid }) { log ->
val result = remember(state.requests) { val result = remember(state.tracerouteRequests) {
state.results.find { it.decoded.requestId == log.fromRadio.packet.id } state.tracerouteResults.find { it.decoded.requestId == log.fromRadio.packet.id }
} }
val route = remember(result) { result?.fullRouteDiscovery } val route = remember(result) { result?.fullRouteDiscovery }