refactor: decouple `NavGraph` from ViewModel and NodeEntity

pull/1444/head
andrekir 2024-11-30 07:50:15 -03:00
rodzic 6678df78b0
commit 716a3f535f
5 zmienionych plików z 69 dodań i 43 usunięć

Wyświetl plik

@ -35,17 +35,20 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging 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.database.entity.NodeEntity
import com.geeksville.mesh.model.map.CustomTileSource import com.geeksville.mesh.model.map.CustomTileSource
import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.ui.Route import com.geeksville.mesh.ui.Route
import com.geeksville.mesh.ui.map.MAP_STYLE_ID import com.geeksville.mesh.ui.map.MAP_STYLE_ID
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -63,6 +66,7 @@ data class MetricsState(
val isManaged: Boolean = true, val isManaged: Boolean = true,
val isFahrenheit: Boolean = false, val isFahrenheit: Boolean = false,
val displayUnits: DisplayUnits = DisplayUnits.METRIC, val displayUnits: DisplayUnits = DisplayUnits.METRIC,
val node: NodeEntity? = null,
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(),
@ -160,6 +164,13 @@ class MetricsViewModel @Inject constructor(
val timeFrame: StateFlow<TimeFrame> = _timeFrame val timeFrame: StateFlow<TimeFrame> = _timeFrame
init { init {
@OptIn(ExperimentalCoroutinesApi::class)
radioConfigRepository.nodeDBbyNum
.mapLatest { nodes -> nodes[destNum] }
.distinctUntilChanged()
.onEach { node -> _state.update { state -> state.copy(node = node) } }
.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach { profile -> radioConfigRepository.deviceProfileFlow.onEach { profile ->
val moduleConfig = profile.moduleConfig val moduleConfig = profile.moduleConfig
_state.update { state -> _state.update { state ->

Wyświetl plik

@ -68,6 +68,7 @@ import javax.inject.Inject
* Data class that represents the current RadioConfig state. * Data class that represents the current RadioConfig state.
*/ */
data class RadioConfigState( data class RadioConfigState(
val isLocal: Boolean = false,
val connected: Boolean = false, val connected: Boolean = false,
val route: String = "", val route: String = "",
val metadata: MeshProtos.DeviceMetadata = MeshProtos.DeviceMetadata.getDefaultInstance(), val metadata: MeshProtos.DeviceMetadata = MeshProtos.DeviceMetadata.getDefaultInstance(),
@ -121,6 +122,10 @@ class RadioConfigViewModel @Inject constructor(
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
radioConfigRepository.myNodeInfo.onEach { ni ->
_radioConfigState.update { it.copy(isLocal = destNum == null || destNum == ni?.myNodeNum) }
}.launchIn(viewModelScope)
debug("RadioConfigViewModel created") debug("RadioConfigViewModel created")
} }

Wyświetl plik

@ -73,7 +73,6 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.model.MetricsViewModel import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.RadioConfigViewModel import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DeviceMetricsScreen import com.geeksville.mesh.ui.components.DeviceMetricsScreen
@ -163,8 +162,6 @@ class NavGraphFragment : ScreenFragment("NavGraph"), Logging {
} }
) { innerPadding -> ) { innerPadding ->
NavGraph( NavGraph(
node = node,
viewModel = model,
navController = navController, navController = navController,
startDestination = startDestination, startDestination = startDestination,
modifier = Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),
@ -290,8 +287,6 @@ private fun MeshAppBar(
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
fun NavGraph( fun NavGraph(
node: NodeEntity?,
viewModel: RadioConfigViewModel = hiltViewModel(),
navController: NavHostController = rememberNavController(), navController: NavHostController = rememberNavController(),
startDestination: Any, startDestination: Any,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -302,9 +297,7 @@ fun NavGraph(
modifier = modifier, modifier = modifier,
) { ) {
composable<Route.NodeDetail> { composable<Route.NodeDetail> {
NodeDetailScreen( NodeDetailScreen { navController.navigate(route = it) }
node = node,
) { navController.navigate(route = it) }
} }
composable<Route.DeviceMetrics> { composable<Route.DeviceMetrics> {
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() } val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
@ -331,79 +324,99 @@ fun NavGraph(
TracerouteLogScreen(hiltViewModel<MetricsViewModel>(parentEntry)) TracerouteLogScreen(hiltViewModel<MetricsViewModel>(parentEntry))
} }
composable<Route.RadioConfig> { composable<Route.RadioConfig> {
RadioConfigScreen( RadioConfigScreen { navController.navigate(route = it) }
node = node,
viewModel = viewModel,
) { navController.navigate(route = it) }
} }
composable<Route.User> { composable<Route.User> {
UserConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
UserConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Channels> { composable<Route.Channels> {
ChannelConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
ChannelConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Device> { composable<Route.Device> {
DeviceConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DeviceConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Position> { composable<Route.Position> {
PositionConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PositionConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Power> { composable<Route.Power> {
PowerConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PowerConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Network> { composable<Route.Network> {
NetworkConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
NetworkConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Display> { composable<Route.Display> {
DisplayConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DisplayConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.LoRa> { composable<Route.LoRa> {
LoRaConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
LoRaConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Bluetooth> { composable<Route.Bluetooth> {
BluetoothConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
BluetoothConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Security> { composable<Route.Security> {
SecurityConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
SecurityConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.MQTT> { composable<Route.MQTT> {
MQTTConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
MQTTConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Serial> { composable<Route.Serial> {
SerialConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
SerialConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.ExtNotification> { composable<Route.ExtNotification> {
ExternalNotificationConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
ExternalNotificationConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.StoreForward> { composable<Route.StoreForward> {
StoreForwardConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
StoreForwardConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.RangeTest> { composable<Route.RangeTest> {
RangeTestConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
RangeTestConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Telemetry> { composable<Route.Telemetry> {
TelemetryConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
TelemetryConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.CannedMessage> { composable<Route.CannedMessage> {
CannedMessageConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
CannedMessageConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Audio> { composable<Route.Audio> {
AudioConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
AudioConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.RemoteHardware> { composable<Route.RemoteHardware> {
RemoteHardwareConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
RemoteHardwareConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.NeighborInfo> { composable<Route.NeighborInfo> {
NeighborInfoConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
NeighborInfoConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.AmbientLighting> { composable<Route.AmbientLighting> {
AmbientLightingConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
AmbientLightingConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.DetectionSensor> { composable<Route.DetectionSensor> {
DetectionSensorConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DetectionSensorConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Paxcounter> { composable<Route.Paxcounter> {
PaxcounterConfigScreen(viewModel) val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PaxcounterConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
} }
} }

Wyświetl plik

@ -98,14 +98,14 @@ import kotlin.math.ln
@Composable @Composable
fun NodeDetailScreen( fun NodeDetailScreen(
node: NodeEntity?,
viewModel: MetricsViewModel = hiltViewModel(), viewModel: MetricsViewModel = hiltViewModel(),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onNavigate: (Any) -> Unit, onNavigate: (Any) -> Unit,
) { ) {
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
if (node != null) { if (state.node != null) {
val node = state.node ?: return
NodeDetailList( NodeDetailList(
node = node, node = node,
metricsState = state, metricsState = state,

Wyświetl plik

@ -34,8 +34,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog import androidx.compose.material.AlertDialog
import androidx.compose.material.Button import androidx.compose.material.Button
@ -65,7 +65,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.model.RadioConfigViewModel import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.PreferenceCategory import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog
@ -79,12 +78,10 @@ private fun getNavRouteFrom(routeName: String): Any? {
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable @Composable
fun RadioConfigScreen( fun RadioConfigScreen(
node: NodeEntity?,
viewModel: RadioConfigViewModel = hiltViewModel(), viewModel: RadioConfigViewModel = hiltViewModel(),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onNavigate: (Any) -> Unit = {} onNavigate: (Any) -> Unit = {}
) { ) {
val isLocal = node?.num == viewModel.myNodeNum
val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
var isWaiting by remember { mutableStateOf(false) } var isWaiting by remember { mutableStateOf(false) }
@ -140,7 +137,7 @@ fun RadioConfigScreen(
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*" type = "application/*"
putExtra(Intent.EXTRA_TITLE, "${node!!.num.toUInt()}.cfg") putExtra(Intent.EXTRA_TITLE, "device_profile.cfg")
} }
exportConfigLauncher.launch(intent) exportConfigLauncher.launch(intent)
} }
@ -154,7 +151,7 @@ fun RadioConfigScreen(
RadioConfigItemList( RadioConfigItemList(
enabled = state.connected && !isWaiting, enabled = state.connected && !isWaiting,
isLocal = isLocal, isLocal = state.isLocal,
modifier = modifier, modifier = modifier,
onRouteClick = { route -> onRouteClick = { route ->
isWaiting = true isWaiting = true