From 4e027ca28e374eb283b7b479395a35286a5d554c Mon Sep 17 00:00:00 2001 From: James Rich Date: Thu, 26 Mar 2026 15:53:15 -0500 Subject: [PATCH] refactor: streamline main screen navigation and ViewModel injection - Update `MainScreen` (Android) and `DesktopMainScreen` to manage their own `NavBackStack` initialization internally. - Refactor `MainScreen` to obtain `UIViewModel` via Koin injection instead of receiving it as a parameter from `MainActivity`. - Remove default bottom padding from `MeshtasticAppShell` and associated screen-level modifiers to allow for more flexible layout orchestration. - Simplify `DesktopMainScreen` by moving backstack management inside the composable and cleaning up the navigation provider logic. - Remove redundant lint suppressions in `Main.kt` following the simplification of the main screen composable structure. - Clean up imports and normalize the usage of `MeshtasticNavDisplay` and `MeshtasticNavigationSuite` across platforms. --- .../kotlin/org/meshtastic/app/MainActivity.kt | 2 +- .../main/kotlin/org/meshtastic/app/ui/Main.kt | 49 +++++++++---------- .../core/ui/component/MeshtasticAppShell.kt | 4 +- .../kotlin/org/meshtastic/desktop/Main.kt | 2 +- .../desktop/ui/DesktopMainScreen.kt | 28 +++++------ .../feature/intro/AppIntroductionScreen.kt | 3 +- 6 files changed, 39 insertions(+), 49 deletions(-) diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt index 66f518d3e..3c3df027f 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt @@ -139,7 +139,7 @@ class MainActivity : ComponentActivity() { ReportDrawnWhen { true } if (appIntroCompleted) { - MainScreen(uIViewModel = model) + MainScreen() } else { val introViewModel = koinViewModel() AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }, viewModel = introViewModel) diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt index 940de1741..d5cd956b8 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt @@ -19,15 +19,12 @@ package org.meshtastic.app.ui import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.recalculateWindowInsets import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider @@ -43,6 +40,7 @@ import org.meshtastic.core.resources.app_too_old import org.meshtastic.core.resources.must_update import org.meshtastic.core.ui.component.MeshtasticAppShell import org.meshtastic.core.ui.component.MeshtasticNavDisplay +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 @@ -52,38 +50,36 @@ import org.meshtastic.feature.node.navigation.nodesGraph import org.meshtastic.feature.settings.navigation.settingsGraph import org.meshtastic.feature.settings.radio.channel.channelsGraph -@OptIn(ExperimentalMaterial3Api::class) -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -fun MainScreen(uIViewModel: UIViewModel = koinViewModel()) { +fun MainScreen() { + val viewModel: UIViewModel = koinViewModel() val backStack = rememberNavBackStack(MeshtasticNavSavedStateConfig, NodesRoutes.NodesGraph as NavKey) - AndroidAppVersionCheck(uIViewModel) + AndroidAppVersionCheck(viewModel) MeshtasticAppShell( backStack = backStack, - uiViewModel = uIViewModel, - hostModifier = Modifier.safeDrawingPadding().padding(bottom = 16.dp), + uiViewModel = viewModel, + hostModifier = Modifier, ) { - org.meshtastic.core.ui.component.MeshtasticNavigationSuite( + MeshtasticNavigationSuite( backStack = backStack, - uiViewModel = uIViewModel, + uiViewModel = viewModel, modifier = Modifier.fillMaxSize(), ) { - val provider = - entryProvider { - contactsGraph(backStack, uIViewModel.scrollToTopEventFlow) - nodesGraph( - backStack = backStack, - scrollToTopEvents = uIViewModel.scrollToTopEventFlow, - onHandleDeepLink = uIViewModel::handleDeepLink, - ) - mapGraph(backStack) - channelsGraph(backStack) - connectionsGraph(backStack) - settingsGraph(backStack) - firmwareGraph(backStack) - } + val provider = entryProvider { + contactsGraph(backStack, viewModel.scrollToTopEventFlow) + nodesGraph( + backStack = backStack, + scrollToTopEvents = viewModel.scrollToTopEventFlow, + onHandleDeepLink = viewModel::handleDeepLink, + ) + mapGraph(backStack) + channelsGraph(backStack) + connectionsGraph(backStack) + settingsGraph(backStack) + firmwareGraph(backStack) + } MeshtasticNavDisplay( backStack = backStack, entryProvider = provider, @@ -99,7 +95,6 @@ private fun AndroidAppVersionCheck(viewModel: UIViewModel) { val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle() - // Check if the device is running an old app version LaunchedEffect(connectionState, myNodeInfo) { if (connectionState == ConnectionState.Connected) { myNodeInfo?.let { info -> @@ -120,4 +115,4 @@ private fun AndroidAppVersionCheck(viewModel: UIViewModel) { } } } -} +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt index 046a22bd0..164305b05 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt @@ -16,11 +16,9 @@ */ package org.meshtastic.core.ui.component -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import org.meshtastic.core.navigation.NodeDetailRoutes @@ -36,7 +34,7 @@ import org.meshtastic.core.ui.viewmodel.UIViewModel fun MeshtasticAppShell( backStack: NavBackStack, uiViewModel: UIViewModel, - hostModifier: Modifier = Modifier.padding(bottom = 16.dp), + hostModifier: Modifier = Modifier, content: @Composable () -> Unit, ) { LaunchedEffect(uiViewModel) { diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt index e326c102d..3f30f9b30 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -321,7 +321,7 @@ fun main(args: Array) = application(exitProcessOnExit = false) { // re-reads Locale.current and all stringResource() calls update. Unlike key(), this // preserves remembered state (including the navigation backstack). CompositionLocalProvider(LocalAppLocale provides localePref) { - AppTheme(darkTheme = isDarkTheme) { DesktopMainScreen(backStack) } + AppTheme(darkTheme = isDarkTheme) { DesktopMainScreen(uiViewModel) } } } } diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt index 4d32bc16c..0280c193f 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt @@ -18,41 +18,37 @@ package org.meshtastic.desktop.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider -import org.koin.compose.viewmodel.koinViewModel +import org.meshtastic.core.navigation.MeshtasticNavSavedStateConfig +import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.ui.component.MeshtasticAppShell import org.meshtastic.core.ui.component.MeshtasticNavDisplay +import org.meshtastic.core.ui.component.MeshtasticNavigationSuite import org.meshtastic.core.ui.viewmodel.UIViewModel import org.meshtastic.desktop.navigation.desktopNavGraph /** - * Desktop main screen — Navigation 3 shell with adaptive navigation and shared [MeshtasticNavDisplay]. - * - * Uses the same shared routes from `core:navigation` and the same `MeshtasticNavDisplay` + `entryProvider` pattern as - * the Android app, proving the shared backstack architecture works across targets. + * Desktop main screen — uses shared navigation components. */ @Composable -@Suppress("LongMethod") -fun DesktopMainScreen(backStack: NavBackStack, uiViewModel: UIViewModel = koinViewModel()) { - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { +fun DesktopMainScreen(uiViewModel: UIViewModel) { + val backStack = androidx.navigation3.runtime.rememberNavBackStack(MeshtasticNavSavedStateConfig, NodesRoutes.NodesGraph as NavKey) + + Surface(modifier = Modifier.fillMaxSize()) { MeshtasticAppShell( backStack = backStack, uiViewModel = uiViewModel, hostModifier = Modifier.padding(bottom = 24.dp), ) { - org.meshtastic.core.ui.component.MeshtasticNavigationSuite( - backStack = backStack, - uiViewModel = uiViewModel, - ) { - val provider = entryProvider { desktopNavGraph(backStack, uiViewModel) } - + MeshtasticNavigationSuite(backStack = backStack, uiViewModel = uiViewModel, modifier = Modifier.fillMaxSize()) { + val provider = entryProvider { + desktopNavGraph(backStack, uiViewModel) + } MeshtasticNavDisplay(backStack = backStack, entryProvider = provider, modifier = Modifier.fillMaxSize()) } } diff --git a/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt index 8437255cf..75756cfaa 100644 --- a/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt @@ -24,6 +24,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberPermissionState +import org.meshtastic.core.ui.component.MeshtasticNavDisplay /** * Main application introduction screen. This Composable hosts the navigation flow and hoists the permission states. @@ -56,7 +57,7 @@ fun AppIntroductionScreen(onDone: () -> Unit, viewModel: IntroViewModel) { val backStack = rememberNavBackStack(Welcome) - org.meshtastic.core.ui.component.MeshtasticNavDisplay( + MeshtasticNavDisplay( backStack = backStack, entryProvider = introNavGraph(