From f2d29d45829f8da5674cbaff0240aa12b66752c8 Mon Sep 17 00:00:00 2001
From: Phil Oliver <3497406+poliver@users.noreply.github.com>
Date: Thu, 18 Sep 2025 12:08:10 -0400
Subject: [PATCH] Flatten `BluetoothViewModel` (#3138)
---
.../java/com/geeksville/mesh/MainActivity.kt | 4 +--
.../mesh/model/BluetoothViewModel.kt | 33 -------------------
.../mesh/navigation/ConnectionsNavigation.kt | 4 +--
.../main/java/com/geeksville/mesh/ui/Main.kt | 9 ++---
.../mesh/ui/connections/ConnectionsScreen.kt | 6 ++--
.../ui/connections/ConnectionsViewModel.kt | 17 +++++-----
6 files changed, 15 insertions(+), 58 deletions(-)
delete mode 100644 app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index d74a173a5..8d0e07f16 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -42,7 +42,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.prefs.UiPrefs
-import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.DEEP_LINK_BASE_URI
import com.geeksville.mesh.ui.MainScreen
@@ -58,7 +57,6 @@ import javax.inject.Inject
class MainActivity :
AppCompatActivity(),
Logging {
- private val bluetoothViewModel: BluetoothViewModel by viewModels()
private val model: UIViewModel by viewModels()
// This is aware of the Activity lifecycle and handles binding to the mesh service.
@@ -108,7 +106,7 @@ class MainActivity :
val appIntroCompleted by model.appIntroCompleted.collectAsStateWithLifecycle()
if (appIntroCompleted) {
- MainScreen(uIViewModel = model, bluetoothViewModel = bluetoothViewModel)
+ MainScreen(uIViewModel = model)
} else {
AppIntroductionScreen(
onDone = {
diff --git a/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt
deleted file mode 100644
index fc28f8e5c..000000000
--- a/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (c) 2025 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 .
- */
-
-package com.geeksville.mesh.model
-
-import androidx.lifecycle.ViewModel
-import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-
-/** Thin view model which adapts the view layer to the `BluetoothRepository`. */
-@HiltViewModel
-class BluetoothViewModel @Inject constructor(private val bluetoothRepository: BluetoothRepository) : ViewModel() {
- /** Called when permissions have been updated. This causes an explicit refresh of the bluetooth state. */
- fun permissionsUpdated() = bluetoothRepository.refreshState()
-
- val enabled = bluetoothRepository.state.map { it.enabled }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt
index d11172362..8a16a7d92 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt
@@ -24,12 +24,11 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
-import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.ui.connections.ConnectionsScreen
import com.geeksville.mesh.ui.settings.radio.components.LoRaConfigScreen
/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoutes.Connections]. */
-fun NavGraphBuilder.connectionsGraph(navController: NavHostController, bluetoothViewModel: BluetoothViewModel) {
+fun NavGraphBuilder.connectionsGraph(navController: NavHostController) {
@Suppress("ktlint:standard:max-line-length")
navigation(startDestination = ConnectionsRoutes.Connections) {
composable(
@@ -40,7 +39,6 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController, bluetooth
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(ConnectionsRoutes.ConnectionsGraph) }
ConnectionsScreen(
- bluetoothViewModel = bluetoothViewModel,
radioConfigViewModel = hiltViewModel(parentEntry),
onClickNodeChip = {
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index 1e69265c4..2d310142a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -81,7 +81,6 @@ import com.geeksville.mesh.android.AddNavigationTracking
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.setAttributes
import com.geeksville.mesh.model.BTScanModel
-import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel
@@ -148,11 +147,7 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector,
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
-fun MainScreen(
- uIViewModel: UIViewModel = hiltViewModel(),
- bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
- scanModel: BTScanModel = hiltViewModel(),
-) {
+fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanModel = hiltViewModel()) {
val navController = rememberNavController()
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
@@ -396,7 +391,7 @@ fun MainScreen(
nodesGraph(navController, uiViewModel = uIViewModel)
mapGraph(navController, uiViewModel = uIViewModel)
channelsGraph(navController, uiViewModel = uIViewModel)
- connectionsGraph(navController, bluetoothViewModel)
+ connectionsGraph(navController)
settingsGraph(navController)
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
index 70d4855ad..23022fa13 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
@@ -62,7 +62,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.model.BTScanModel
-import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.navigation.ConfigRoute
@@ -102,7 +101,6 @@ fun String?.isIPAddress(): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_C
fun ConnectionsScreen(
connectionsViewModel: ConnectionsViewModel = hiltViewModel(),
scanModel: BTScanModel = hiltViewModel(),
- bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
onClickNodeChip: (Int) -> Unit,
onNavigateToSettings: () -> Unit,
@@ -120,7 +118,7 @@ fun ConnectionsScreen(
val info by connectionsViewModel.myNodeInfo.collectAsStateWithLifecycle()
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
- val bluetoothEnabled by bluetoothViewModel.enabled.collectAsStateWithLifecycle(false)
+ val bluetoothState by connectionsViewModel.bluetoothState.collectAsStateWithLifecycle()
val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
@@ -264,7 +262,7 @@ fun ConnectionsScreen(
btDevices = bleDevices,
selectedDevice = selectedDevice,
scanModel = scanModel,
- bluetoothEnabled = bluetoothEnabled,
+ bluetoothEnabled = bluetoothState.enabled,
)
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt
index 3c2a27244..491aee075 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt
@@ -24,6 +24,7 @@ import com.geeksville.mesh.android.prefs.UiPrefs
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.database.entity.MyNodeEntity
import com.geeksville.mesh.model.Node
+import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,8 +38,9 @@ import javax.inject.Inject
class ConnectionsViewModel
@Inject
constructor(
- private val radioConfigRepository: RadioConfigRepository,
- private val nodeRepository: NodeRepository,
+ radioConfigRepository: RadioConfigRepository,
+ nodeRepository: NodeRepository,
+ bluetoothRepository: BluetoothRepository,
private val uiPrefs: UiPrefs,
) : ViewModel() {
val localConfig: StateFlow =
@@ -48,14 +50,13 @@ constructor(
LocalConfig.getDefaultInstance(),
)
- val connectionState
- get() = radioConfigRepository.connectionState
+ val connectionState = radioConfigRepository.connectionState
- val myNodeInfo: StateFlow
- get() = nodeRepository.myNodeInfo
+ val myNodeInfo: StateFlow = nodeRepository.myNodeInfo
- val ourNodeInfo: StateFlow
- get() = nodeRepository.ourNodeInfo
+ val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo
+
+ val bluetoothState = bluetoothRepository.state
private val _hasShownNotPairedWarning = MutableStateFlow(uiPrefs.hasShownNotPairedWarning)
val hasShownNotPairedWarning: StateFlow = _hasShownNotPairedWarning.asStateFlow()