refactor: pass nav args to ViewModel using SavedStateHandle

This commit is contained in:
andrekir 2024-11-09 06:46:47 -03:00 committed by Andre K
parent 296f1944b7
commit 1ae65ef267
4 changed files with 38 additions and 64 deletions

View file

@ -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<Route.NodeDetail>().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.
*/

View file

@ -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<Int?>(null)
private val destNum = savedStateHandle.toRoute<Route.RadioConfig>().destNum
private val _destNode = MutableStateFlow<NodeEntity?>(null)
val destNode: StateFlow<NodeEntity?> 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<Int>())
private val _radioConfigState = MutableStateFlow(RadioConfigState())
val radioConfigState: StateFlow<RadioConfigState> = _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)

View file

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

View file

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