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.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<Telemetry> = emptyList(),
val environmentMetrics: List<Telemetry> = emptyList(),
val signalMetrics: List<MeshPacket> = emptyList(),
val hasTracerouteLogs: Boolean = false,
val environmentDisplayFahrenheit: Boolean = false,
val tracerouteRequests: List<MeshLog> = emptyList(),
val tracerouteResults: List<MeshPacket> = 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<MeshLog> = emptyList(),
val results: List<MeshPacket> = 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<MetricsState> = _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.

Wyświetl plik

@ -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")
}

Wyświetl plik

@ -83,7 +83,7 @@ fun EnvironmentMetricsScreen(
return (celsius * 1.8F) + 32
}
val processedTelemetries: List<Telemetry> = if (state.environmentDisplayFahrenheit) {
val processedTelemetries: List<Telemetry> = 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
)
}
}

Wyświetl plik

@ -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 }