mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-28 02:32:24 -04:00
feat: refactor adaptive navigation using NavigationSuiteScaffold
- Integrate the `material3-adaptive-navigation-suite` library to streamline cross-platform navigation layouts. - Replace manual branching logic for `NavigationBar` and `NavigationRail` with `NavigationSuiteScaffold` in `MeshtasticNavigationSuite`. - Implement a `coerceNavigationType` helper to ensure `NavigationRail` is used for expanded widths instead of promoting to a `NavigationDrawer`. - Utilize `currentWindowAdaptiveInfo` with large-width breakpoint support to improve responsiveness on Desktop and external displays. - Remove redundant `MeshtasticNavigationBar` and `MeshtasticNavigationRail` private composables in favor of the standard navigation suite item API.
This commit is contained in:
@@ -54,6 +54,7 @@ kotlin {
|
||||
implementation(libs.jetbrains.compose.material3.adaptive)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.layout)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite)
|
||||
implementation(libs.jetbrains.navigationevent.compose)
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
}
|
||||
|
||||
@@ -22,27 +22,21 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationRail
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -53,7 +47,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.window.core.layout.WindowWidthSizeClass
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
@@ -70,8 +63,11 @@ import org.meshtastic.core.ui.navigation.icon
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
|
||||
/**
|
||||
* Shared adaptive navigation shell. Provides a Bottom Navigation bar on phones, and a Navigation Rail on tablets and
|
||||
* desktop targets.
|
||||
* Shared adaptive navigation shell using [NavigationSuiteScaffold].
|
||||
*
|
||||
* Automatically renders a [NavigationBar][androidx.compose.material3.NavigationBar] on compact screens and a
|
||||
* [NavigationRail][androidx.compose.material3.NavigationRail] on medium/expanded widths, without manual branching. Uses
|
||||
* [currentWindowAdaptiveInfo] with large-width breakpoint support for Desktop and External Display targets.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -86,7 +82,6 @@ fun MeshtasticNavigationSuite(
|
||||
val selectedDevice by uiViewModel.currentDeviceAddressFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val adaptiveInfo = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)
|
||||
val isCompact = adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
|
||||
val currentKey = backStack.lastOrNull()
|
||||
val rootKey = backStack.firstOrNull()
|
||||
val topLevelDestination = TopLevelDestination.fromNavKey(rootKey)
|
||||
@@ -95,37 +90,48 @@ fun MeshtasticNavigationSuite(
|
||||
handleNavigation(destination, topLevelDestination, currentKey, backStack, uiViewModel)
|
||||
}
|
||||
|
||||
if (isCompact) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
bottomBar = {
|
||||
MeshtasticNavigationBar(
|
||||
topLevelDestination = topLevelDestination,
|
||||
connectionState = connectionState,
|
||||
unreadMessageCount = unreadMessageCount,
|
||||
selectedDevice = selectedDevice,
|
||||
uiViewModel = uiViewModel,
|
||||
onNavigate = onNavigate,
|
||||
// Cap the layout type at NavigationRail for expanded widths — we don't want a permanent
|
||||
// NavigationDrawer. NavigationSuiteScaffoldDefaults resolves COMPACT → NavigationBar,
|
||||
// MEDIUM/EXPANDED → NavigationRail already; passing the custom adaptiveInfo ensures the
|
||||
// large-width (1200dp+) breakpoints are respected.
|
||||
val layoutType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo).coerceNavigationType()
|
||||
|
||||
NavigationSuiteScaffold(
|
||||
modifier = modifier,
|
||||
layoutType = layoutType,
|
||||
navigationSuiteItems = {
|
||||
TopLevelDestination.entries.forEach { destination ->
|
||||
item(
|
||||
selected = destination == topLevelDestination,
|
||||
onClick = { onNavigate(destination) },
|
||||
icon = {
|
||||
NavigationIconContent(
|
||||
destination = destination,
|
||||
isSelected = destination == topLevelDestination,
|
||||
connectionState = connectionState,
|
||||
unreadMessageCount = unreadMessageCount,
|
||||
selectedDevice = selectedDevice,
|
||||
uiViewModel = uiViewModel,
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(padding)) { content() }
|
||||
}
|
||||
} else {
|
||||
Row(modifier = modifier.fillMaxSize()) {
|
||||
MeshtasticNavigationRail(
|
||||
topLevelDestination = topLevelDestination,
|
||||
connectionState = connectionState,
|
||||
unreadMessageCount = unreadMessageCount,
|
||||
selectedDevice = selectedDevice,
|
||||
uiViewModel = uiViewModel,
|
||||
onNavigate = onNavigate,
|
||||
)
|
||||
Box(modifier = Modifier.weight(1f).fillMaxSize()) { content() }
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caps [NavigationSuiteType] so that expanded/extra-large widths still use a NavigationRail instead of promoting to a
|
||||
* permanent NavigationDrawer.
|
||||
*/
|
||||
private fun NavigationSuiteType.coerceNavigationType(): NavigationSuiteType = when (this) {
|
||||
NavigationSuiteType.NavigationDrawer -> NavigationSuiteType.NavigationRail
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun handleNavigation(
|
||||
destination: TopLevelDestination,
|
||||
topLevelDestination: TopLevelDestination?,
|
||||
@@ -164,65 +170,6 @@ private fun handleNavigation(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MeshtasticNavigationBar(
|
||||
topLevelDestination: TopLevelDestination?,
|
||||
connectionState: ConnectionState,
|
||||
unreadMessageCount: Int,
|
||||
selectedDevice: String?,
|
||||
uiViewModel: UIViewModel,
|
||||
onNavigate: (TopLevelDestination) -> Unit,
|
||||
) {
|
||||
NavigationBar {
|
||||
TopLevelDestination.entries.forEach { destination ->
|
||||
NavigationBarItem(
|
||||
selected = destination == topLevelDestination,
|
||||
onClick = { onNavigate(destination) },
|
||||
icon = {
|
||||
NavigationIconContent(
|
||||
destination = destination,
|
||||
isSelected = destination == topLevelDestination,
|
||||
connectionState = connectionState,
|
||||
unreadMessageCount = unreadMessageCount,
|
||||
selectedDevice = selectedDevice,
|
||||
uiViewModel = uiViewModel,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MeshtasticNavigationRail(
|
||||
topLevelDestination: TopLevelDestination?,
|
||||
connectionState: ConnectionState,
|
||||
unreadMessageCount: Int,
|
||||
selectedDevice: String?,
|
||||
uiViewModel: UIViewModel,
|
||||
onNavigate: (TopLevelDestination) -> Unit,
|
||||
) {
|
||||
NavigationRail {
|
||||
TopLevelDestination.entries.forEach { destination ->
|
||||
NavigationRailItem(
|
||||
selected = destination == topLevelDestination,
|
||||
onClick = { onNavigate(destination) },
|
||||
icon = {
|
||||
NavigationIconContent(
|
||||
destination = destination,
|
||||
isSelected = destination == topLevelDestination,
|
||||
connectionState = connectionState,
|
||||
unreadMessageCount = unreadMessageCount,
|
||||
selectedDevice = selectedDevice,
|
||||
uiViewModel = uiViewModel,
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun NavigationIconContent(
|
||||
|
||||
@@ -140,6 +140,7 @@ compose-multiplatform-materialIconsExtended = { module = "org.jetbrains.compose.
|
||||
jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" }
|
||||
jetbrains-compose-material3-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "jetbrains-adaptive" }
|
||||
jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "jetbrains-adaptive" }
|
||||
jetbrains-compose-material3-adaptive-navigation-suite = { module = "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", version.ref = "compose-multiplatform-material3" }
|
||||
|
||||
# Google
|
||||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||
|
||||
Reference in New Issue
Block a user