mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 00:28:20 -04:00
refactor: decouple feature/connections from feature/settings
- Move ResponseState<T> to core/model - Move PacketResponseStateDialog to core/ui/component - Create RadioConfigStateProvider interface in core/model - RadioConfigViewModel implements RadioConfigStateProvider - ConnectionsScreen accepts interface instead of concrete VM - ConnectionsNavigation receives provider via lambda from app/desktop - Remove projects.feature.settings dependency from connections build This eliminates a feature→feature dependency, improving build parallelism and enforcing proper module boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -44,6 +44,7 @@ import org.meshtastic.core.ui.component.MeshtasticNavigationSuite
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
import org.meshtastic.feature.connections.navigation.connectionsGraph
|
||||
import org.meshtastic.feature.firmware.navigation.firmwareGraph
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.map.navigation.mapGraph
|
||||
import org.meshtastic.feature.messaging.navigation.contactsGraph
|
||||
import org.meshtastic.feature.node.navigation.nodesGraph
|
||||
@@ -86,7 +87,7 @@ fun MainScreen() {
|
||||
)
|
||||
mapGraph(backStack)
|
||||
channelsGraph(backStack)
|
||||
connectionsGraph(backStack)
|
||||
connectionsGraph(backStack) { koinViewModel<RadioConfigViewModel>() }
|
||||
settingsGraph(backStack)
|
||||
firmwareGraph(backStack)
|
||||
wifiProvisionGraph(backStack)
|
||||
|
||||
@@ -21,9 +21,12 @@ import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.model.RadioConfigStateProvider
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
import org.meshtastic.core.navigation.NodesRoute
|
||||
import org.meshtastic.feature.connections.navigation.connectionsGraph
|
||||
import org.meshtastic.feature.firmware.navigation.firmwareGraph
|
||||
@@ -49,7 +52,14 @@ class NavigationAssemblyTest {
|
||||
nodesGraph(backStack = backStack, scrollToTopEvents = emptyFlow())
|
||||
mapGraph(backStack)
|
||||
channelsGraph(backStack)
|
||||
connectionsGraph(backStack)
|
||||
connectionsGraph(backStack) {
|
||||
object : RadioConfigStateProvider {
|
||||
override val packetResponseState = MutableStateFlow<ResponseState<Boolean>>(ResponseState.Empty)
|
||||
override val pendingRouteName = MutableStateFlow("")
|
||||
override fun requestConfigLoad(routeName: String) {}
|
||||
override fun clearPacketResponse() {}
|
||||
}
|
||||
}
|
||||
settingsGraph(backStack)
|
||||
firmwareGraph(backStack)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Minimal interface exposing radio-config packet response state.
|
||||
* Used by feature/connections to observe config-loading progress without
|
||||
* depending on the full RadioConfigViewModel in feature/settings.
|
||||
*/
|
||||
interface RadioConfigStateProvider {
|
||||
/** Current packet response state (loading/success/error/empty). */
|
||||
val packetResponseState: StateFlow<ResponseState<Boolean>>
|
||||
|
||||
/** Route name associated with the pending config request (e.g. "LORA"). */
|
||||
val pendingRouteName: StateFlow<String>
|
||||
|
||||
/** Initiate a config load for the given route name. */
|
||||
fun requestConfigLoad(routeName: String)
|
||||
|
||||
/** Clear the packet response, resetting to [ResponseState.Empty]. */
|
||||
fun clearPacketResponse()
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.radio
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.meshtastic.core.resources.UiText
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.radio.component
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -36,17 +36,16 @@ import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.MetricFormatter
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.resources.delivery_confirmed
|
||||
import org.meshtastic.core.resources.delivery_confirmed_reboot_warning
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.icon.Error
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Success
|
||||
import org.meshtastic.feature.settings.radio.ResponseState
|
||||
|
||||
private const val AUTO_DISMISS_DELAY_MS = 1500L
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.meshtastic.desktop.navigation
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.core.navigation.MultiBackstack
|
||||
import org.meshtastic.core.navigation.TopLevelDestination
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
@@ -28,6 +29,7 @@ import org.meshtastic.feature.map.navigation.mapGraph
|
||||
import org.meshtastic.feature.messaging.navigation.contactsGraph
|
||||
import org.meshtastic.feature.node.navigation.nodesGraph
|
||||
import org.meshtastic.feature.settings.navigation.settingsGraph
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.channel.channelsGraph
|
||||
import org.meshtastic.feature.wifiprovision.navigation.wifiProvisionGraph
|
||||
|
||||
@@ -53,6 +55,6 @@ fun EntryProviderScope<NavKey>.desktopNavGraph(
|
||||
firmwareGraph(backStack)
|
||||
settingsGraph(backStack)
|
||||
channelsGraph(backStack)
|
||||
connectionsGraph(backStack)
|
||||
connectionsGraph(backStack) { koinViewModel<RadioConfigViewModel>() }
|
||||
wifiProvisionGraph(backStack)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ kotlin {
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.ble)
|
||||
implementation(projects.core.network)
|
||||
implementation(projects.feature.settings)
|
||||
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
}
|
||||
|
||||
@@ -16,22 +16,26 @@
|
||||
*/
|
||||
package org.meshtastic.feature.connections.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.core.model.RadioConfigStateProvider
|
||||
import org.meshtastic.core.navigation.ConnectionsRoute
|
||||
import org.meshtastic.core.navigation.NodesRoute
|
||||
import org.meshtastic.feature.connections.ScannerViewModel
|
||||
import org.meshtastic.feature.connections.ui.ConnectionsScreen
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
|
||||
/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoute.Connections]. */
|
||||
fun EntryProviderScope<NavKey>.connectionsGraph(backStack: NavBackStack<NavKey>) {
|
||||
fun EntryProviderScope<NavKey>.connectionsGraph(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
radioConfigStateProvider: @Composable () -> RadioConfigStateProvider,
|
||||
) {
|
||||
entry<ConnectionsRoute.ConnectionsGraph> {
|
||||
ConnectionsScreen(
|
||||
scanModel = koinViewModel<ScannerViewModel>(),
|
||||
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
|
||||
radioConfigStateProvider = radioConfigStateProvider(),
|
||||
onClickNodeChip = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
|
||||
onNavigateToNodeDetails = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
|
||||
onConfigNavigate = { route -> backStack.add(route) },
|
||||
@@ -41,7 +45,7 @@ fun EntryProviderScope<NavKey>.connectionsGraph(backStack: NavBackStack<NavKey>)
|
||||
entry<ConnectionsRoute.Connections> {
|
||||
ConnectionsScreen(
|
||||
scanModel = koinViewModel<ScannerViewModel>(),
|
||||
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
|
||||
radioConfigStateProvider = radioConfigStateProvider(),
|
||||
onClickNodeChip = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
|
||||
onNavigateToNodeDetails = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
|
||||
onConfigNavigate = { route -> backStack.add(route) },
|
||||
|
||||
@@ -78,10 +78,8 @@ import org.meshtastic.feature.connections.ui.components.ConnectingDeviceInfo
|
||||
import org.meshtastic.feature.connections.ui.components.CurrentlyConnectedInfo
|
||||
import org.meshtastic.feature.connections.ui.components.DeviceList
|
||||
import org.meshtastic.feature.connections.ui.components.TransportFilterChips
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.core.model.RadioConfigStateProvider
|
||||
import org.meshtastic.core.ui.component.PacketResponseStateDialog
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
/**
|
||||
@@ -98,12 +96,13 @@ private val CardMinHeight = 100.dp
|
||||
fun ConnectionsScreen(
|
||||
connectionsViewModel: ConnectionsViewModel = koinViewModel(),
|
||||
scanModel: ScannerViewModel = koinViewModel(),
|
||||
radioConfigViewModel: RadioConfigViewModel = koinViewModel(),
|
||||
radioConfigStateProvider: RadioConfigStateProvider,
|
||||
onClickNodeChip: (Int) -> Unit,
|
||||
onNavigateToNodeDetails: (Int) -> Unit,
|
||||
onConfigNavigate: (Route) -> Unit,
|
||||
) {
|
||||
val radioConfigState by radioConfigViewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val responseState by radioConfigStateProvider.packetResponseState.collectAsStateWithLifecycle()
|
||||
val pendingRoute by radioConfigStateProvider.pendingRouteName.collectAsStateWithLifecycle()
|
||||
val connectionProgress by scanModel.connectionProgressText.collectAsStateWithLifecycle()
|
||||
val connectionStatus by connectionsViewModel.connectionStatus.collectAsStateWithLifecycle()
|
||||
val connectionState by connectionsViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
@@ -153,18 +152,16 @@ fun ConnectionsScreen(
|
||||
var isWaiting by remember { mutableStateOf(false) }
|
||||
if (isWaiting) {
|
||||
PacketResponseStateDialog(
|
||||
state = radioConfigState.responseState,
|
||||
state = responseState,
|
||||
onDismiss = {
|
||||
isWaiting = false
|
||||
radioConfigViewModel.clearPacketResponse()
|
||||
radioConfigStateProvider.clearPacketResponse()
|
||||
},
|
||||
onComplete = {
|
||||
getNavRouteFrom(radioConfigState.route)?.let { route ->
|
||||
if (pendingRoute == "LORA") {
|
||||
isWaiting = false
|
||||
radioConfigViewModel.clearPacketResponse()
|
||||
if (route == SettingsRoute.LoRa) {
|
||||
onConfigNavigate(SettingsRoute.LoRa)
|
||||
}
|
||||
radioConfigStateProvider.clearPacketResponse()
|
||||
onConfigNavigate(SettingsRoute.LoRa)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -264,7 +261,7 @@ fun ConnectionsScreen(
|
||||
text = stringResource(Res.string.set_your_region),
|
||||
onClick = {
|
||||
isWaiting = true
|
||||
radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
|
||||
radioConfigStateProvider.requestConfigLoad("LORA")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ import org.meshtastic.feature.settings.component.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.radio.AdminRoute
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigState
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.ResponseState
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
import org.meshtastic.feature.settings.radio.component.LoadingOverlay
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.core.ui.component.PacketResponseStateDialog
|
||||
import org.meshtastic.feature.settings.radio.component.ShutdownConfirmationDialog
|
||||
import org.meshtastic.feature.settings.radio.component.WarningDialog
|
||||
|
||||
|
||||
@@ -22,12 +22,15 @@ import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
@@ -48,6 +51,7 @@ import org.meshtastic.core.model.MqttProbeStatus
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.RadioConfigStateProvider
|
||||
import org.meshtastic.core.repository.AnalyticsPrefs
|
||||
import org.meshtastic.core.repository.FileService
|
||||
import org.meshtastic.core.repository.HomoglyphPrefs
|
||||
@@ -80,6 +84,7 @@ import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
|
||||
/** Data class that represents the current RadioConfig state. */
|
||||
@androidx.compose.runtime.Immutable
|
||||
@@ -124,7 +129,7 @@ open class RadioConfigViewModel(
|
||||
private val locationService: LocationService,
|
||||
private val fileService: FileService,
|
||||
private val mqttManager: MqttManager,
|
||||
) : ViewModel() {
|
||||
) : ViewModel(), RadioConfigStateProvider {
|
||||
val analyticsAllowedFlow = analyticsPrefs.analyticsAllowed
|
||||
|
||||
fun toggleAnalyticsAllowed() {
|
||||
@@ -398,10 +403,29 @@ open class RadioConfigViewModel(
|
||||
safeLaunch(tag = "installProfile") { installProfileUseCase(destNum, protobuf, destNode.value?.user) }
|
||||
}
|
||||
|
||||
fun clearPacketResponse() {
|
||||
// region RadioConfigStateProvider implementation
|
||||
|
||||
override val packetResponseState: StateFlow<ResponseState<Boolean>> =
|
||||
_radioConfigState.map { it.responseState }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ResponseState.Empty)
|
||||
|
||||
override val pendingRouteName: StateFlow<String> =
|
||||
_radioConfigState.map { it.route }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), "")
|
||||
|
||||
override fun requestConfigLoad(routeName: String) {
|
||||
val route = ConfigRoute.entries.find { it.name == routeName }
|
||||
?: ModuleRoute.entries.find { it.name == routeName }
|
||||
?: return
|
||||
setResponseStateLoading(route)
|
||||
}
|
||||
|
||||
override fun clearPacketResponse() {
|
||||
_radioConfigState.update { it.copy(responseState = ResponseState.Empty) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
fun setResponseStateLoading(route: Enum<*>) {
|
||||
val destNum = destNumFlow.value ?: destNode.value?.num ?: return
|
||||
|
||||
|
||||
@@ -65,14 +65,14 @@ import org.meshtastic.core.ui.component.rememberDragDropState
|
||||
import org.meshtastic.core.ui.icon.Add
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.ResponseState
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
import org.meshtastic.feature.settings.radio.channel.component.ChannelCard
|
||||
import org.meshtastic.feature.settings.radio.channel.component.ChannelConfigHeader
|
||||
import org.meshtastic.feature.settings.radio.channel.component.ChannelLegend
|
||||
import org.meshtastic.feature.settings.radio.channel.component.ChannelLegendDialog
|
||||
import org.meshtastic.feature.settings.radio.channel.component.EditChannelDialog
|
||||
import org.meshtastic.feature.settings.radio.component.LoadingOverlay
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.core.ui.component.PacketResponseStateDialog
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ import org.meshtastic.feature.settings.channel.ChannelViewModel
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.core.ui.component.PacketResponseStateDialog
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@@ -40,7 +40,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.common.util.MetricFormatter
|
||||
import org.meshtastic.feature.settings.radio.ResponseState
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
|
||||
private const val LOADING_OVERLAY_ALPHA = 0.8f
|
||||
private const val PERCENTAGE_FACTOR = 100
|
||||
|
||||
@@ -40,8 +40,9 @@ import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.discard_changes
|
||||
import org.meshtastic.core.resources.save_changes
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.PacketResponseStateDialog
|
||||
import org.meshtastic.core.ui.component.PreferenceFooter
|
||||
import org.meshtastic.feature.settings.radio.ResponseState
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user