From e9c28b71e7fab9ea4881ff34848a72d2de792f15 Mon Sep 17 00:00:00 2001 From: John O'Reilly Date: Tue, 18 Jun 2024 19:42:43 +0100 Subject: [PATCH] Use NavigationSuiteScaffold and ListDetailPaneScaffold --- app/build.gradle.kts | 6 +- .../com/surrus/peopleinspace/MainActivity.kt | 6 +- .../navigation/PeopleInSpaceNavHost.kt | 47 ---- .../navigation/TopLevelDestination.kt | 10 - .../persondetails/PersonDetailsScreen.kt | 12 +- .../peopleinspace/ui/PeopleInSpaceApp.kt | 209 +++++++----------- .../peopleinspace/ui/PeopleInSpaceAppState.kt | 59 ----- gradle/libs.versions.toml | 20 +- 8 files changed, 106 insertions(+), 263 deletions(-) delete mode 100644 app/src/main/java/com/surrus/peopleinspace/navigation/PeopleInSpaceNavHost.kt delete mode 100644 app/src/main/java/com/surrus/peopleinspace/navigation/TopLevelDestination.kt delete mode 100644 app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceAppState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8755393f..9002732b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,7 +73,11 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material3.windowSizeClass) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation.suite) + implementation(libs.coilCompose) implementation(libs.glance.appwidget) diff --git a/app/src/main/java/com/surrus/peopleinspace/MainActivity.kt b/app/src/main/java/com/surrus/peopleinspace/MainActivity.kt index 9891422e..a15e1793 100644 --- a/app/src/main/java/com/surrus/peopleinspace/MainActivity.kt +++ b/app/src/main/java/com/surrus/peopleinspace/MainActivity.kt @@ -3,15 +3,13 @@ package com.surrus.peopleinspace import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.activity.enableEdgeToEdge import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import com.surrus.peopleinspace.ui.PeopleInSpaceApp class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) @@ -21,7 +19,7 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { - PeopleInSpaceApp(calculateWindowSizeClass(this)) + PeopleInSpaceApp() } } } diff --git a/app/src/main/java/com/surrus/peopleinspace/navigation/PeopleInSpaceNavHost.kt b/app/src/main/java/com/surrus/peopleinspace/navigation/PeopleInSpaceNavHost.kt deleted file mode 100644 index 29ced4e7..00000000 --- a/app/src/main/java/com/surrus/peopleinspace/navigation/PeopleInSpaceNavHost.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.surrus.peopleinspace.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.surrus.common.remote.Assignment -import com.surrus.peopleinspace.issposition.ISSPositionRoute -import com.surrus.peopleinspace.persondetails.PersonDetailsScreen -import com.surrus.peopleinspace.personlist.PersonListRoute -import kotlinx.serialization.Serializable - - -@Serializable -object PersonList - -@Serializable -object IssPosition - - -@Composable -fun PeopleInSpaceNavHost( - navController: NavHostController, - onBackClick: () -> Unit = {}, - modifier: Modifier = Modifier -) { - NavHost( - navController = navController, - startDestination = PersonList, - modifier = modifier, - ) { - composable { - PersonListRoute { person -> - navController.navigate(person) - } - } - composable { backStackEntry -> - val person: Assignment = backStackEntry.toRoute() - PersonDetailsScreen(person, onBackClick) - } - composable { - ISSPositionRoute() - } - } -} diff --git a/app/src/main/java/com/surrus/peopleinspace/navigation/TopLevelDestination.kt b/app/src/main/java/com/surrus/peopleinspace/navigation/TopLevelDestination.kt deleted file mode 100644 index e0e80a09..00000000 --- a/app/src/main/java/com/surrus/peopleinspace/navigation/TopLevelDestination.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.surrus.peopleinspace.navigation - -import androidx.compose.ui.graphics.vector.ImageVector - -data class TopLevelDestination( - val route: Any, - val selectedIcon: ImageVector, - val unselectedIcon: ImageVector, - val iconTextId: Int -) diff --git a/app/src/main/java/com/surrus/peopleinspace/persondetails/PersonDetailsScreen.kt b/app/src/main/java/com/surrus/peopleinspace/persondetails/PersonDetailsScreen.kt index f1122ce2..ebd758b0 100644 --- a/app/src/main/java/com/surrus/peopleinspace/persondetails/PersonDetailsScreen.kt +++ b/app/src/main/java/com/surrus/peopleinspace/persondetails/PersonDetailsScreen.kt @@ -32,11 +32,11 @@ import com.surrus.peopleinspace.ui.component.PeopleInSpaceGradientBackground @Composable -fun PersonDetailsScreen(person: Assignment, popBack: () -> Unit) { +fun PersonDetailsScreen(person: Assignment, showBackButton: Boolean, popBack: () -> Unit) { PeopleInSpaceGradientBackground { Scaffold( topBar = { - PersonDetailsTopAppBar(personName = person.name, popBack = popBack) + PersonDetailsTopAppBar(personName = person.name, showBackButton, popBack = popBack) }, containerColor = Color.Transparent, contentWindowInsets = WindowInsets(0, 0, 0, 0) @@ -47,12 +47,14 @@ fun PersonDetailsScreen(person: Assignment, popBack: () -> Unit) { } @Composable -fun PersonDetailsTopAppBar(personName: String, popBack: () -> Unit) { +fun PersonDetailsTopAppBar(personName: String, showBackButton: Boolean, popBack: () -> Unit) { CenterAlignedTopAppBar( title = { Text(personName) }, navigationIcon = { - IconButton(onClick = { popBack() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + if (showBackButton) { + IconButton(onClick = { popBack() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } } }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( diff --git a/app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceApp.kt b/app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceApp.kt index 418829e0..f56609d8 100644 --- a/app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceApp.kt +++ b/app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceApp.kt @@ -1,153 +1,102 @@ package com.surrus.peopleinspace.ui -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import com.surrus.peopleinspace.navigation.PeopleInSpaceNavHost -import com.surrus.peopleinspace.navigation.TopLevelDestination -import com.surrus.peopleinspace.ui.component.PeopleInSpaceBackground +import com.surrus.common.remote.Assignment +import com.surrus.peopleinspace.R +import com.surrus.peopleinspace.issposition.ISSPositionRoute +import com.surrus.peopleinspace.persondetails.PersonDetailsScreen +import com.surrus.peopleinspace.personlist.PersonListRoute -@Composable -fun PeopleInSpaceApp( - windowSizeClass: WindowSizeClass, - appState: PeopleInSpaceAppState = rememberPeopleInSpaceAppState(windowSizeClass) +enum class AppDestinations( + @StringRes val label: Int, + val icon: ImageVector, + @StringRes val contentDescription: Int ) { - val navController = rememberNavController() + PERSON_LIST(R.string.people, Icons.Default.Person, R.string.people), + ISS_POSITION(R.string.iss_position, Icons.Default.LocationOn, R.string.iss_position), +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun PeopleInSpaceApp() { + var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.PERSON_LIST) } + val navigator = rememberListDetailPaneScaffoldNavigator() + + BackHandler(navigator.canNavigateBack()) { + navigator.navigateBack() + } PeopleInSpaceTheme { - PeopleInSpaceBackground { - Scaffold( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground, - contentWindowInsets = WindowInsets(0, 0, 0, 0), - bottomBar = { - if (appState.shouldShowBottomBar) { - PeopleInSpaceBottomBar( - navController = navController, - destinations = appState.topLevelDestinations - ) - } - } - ) { padding -> - Row( - Modifier - .fillMaxSize() - .windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal + NavigationSuiteScaffold( + navigationSuiteItems = { + AppDestinations.entries.forEach { + item( + icon = { + Icon( + it.icon, + contentDescription = stringResource(it.contentDescription) ) - ) - ) { - if (appState.shouldShowNavRail) { - PeopleInSpaceNavRail( - navController = navController, - destinations = appState.topLevelDestinations, - modifier = Modifier.safeDrawingPadding() - ) - } - - PeopleInSpaceNavHost( - navController = navController, - onBackClick = { navController.popBackStack() }, - modifier = Modifier - .padding(padding) - .windowInsetsPadding(WindowInsets.statusBars) + }, + label = { Text(stringResource(it.label)) }, + selected = it == currentDestination, + onClick = { currentDestination = it } ) } } - } - } -} - - -@Composable -private fun PeopleInSpaceNavRail( - navController: NavController, - destinations: List, - modifier: Modifier = Modifier, -) { - - NavigationRail( - modifier = modifier, - containerColor = Color.Transparent, - contentColor = PeopleInSpaceNavigationDefaults.navigationContentColor(), - ) { - destinations.forEach { destination -> - val currentDestination = navController.currentBackStackEntryAsState().value?.destination - val selected = currentDestination?.route == destination.route::class.qualifiedName - NavigationRailItem( - selected = selected, - onClick = { navController.navigate(destination.route) }, - icon = { - val icon = if (selected) { - destination.selectedIcon - } else { - destination.unselectedIcon - } - Icon(icon, contentDescription = stringResource(destination.iconTextId)) + ) { + when (currentDestination) { + AppDestinations.PERSON_LIST -> { + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + PersonListRoute { person -> + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, person) + } + }, + detailPane = { + navigator.currentDestination?.content?.let { + PersonDetailsScreen( + person = it, + showBackButton = !navigator.isListPaneVisible(), + navigator::navigateBack + ) + } + } + ) + } + AppDestinations.ISS_POSITION -> { + ISSPositionRoute() } - ) + } } } } -@Composable -private fun PeopleInSpaceBottomBar( - navController: NavController, - destinations: List -) { - NavigationBar( - contentColor = PeopleInSpaceNavigationDefaults.navigationContentColor(), - tonalElevation = 0.dp, - ) { - destinations.forEach { destination -> - val currentDestination = navController.currentBackStackEntryAsState().value?.destination - val selected = currentDestination?.route == destination.route::class.qualifiedName - NavigationBarItem( - selected = selected, - onClick = { navController.navigate(destination.route) }, - icon = { - val icon = if (selected) { - destination.selectedIcon - } else { - destination.unselectedIcon - } - Icon(icon, contentDescription = stringResource(destination.iconTextId)) - }, - label = { Text(stringResource(destination.iconTextId)) } - ) - } - } -} +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +private fun ThreePaneScaffoldNavigator.isListPaneVisible(): Boolean = + scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded -object PeopleInSpaceNavigationDefaults { - @Composable - fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant -} diff --git a/app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceAppState.kt b/app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceAppState.kt deleted file mode 100644 index 52d3c096..00000000 --- a/app/src/main/java/com/surrus/peopleinspace/ui/PeopleInSpaceAppState.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.surrus.peopleinspace.ui - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.LocationOn -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.outlined.LocationOn -import androidx.compose.material.icons.outlined.Person -import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import com.surrus.peopleinspace.R -import com.surrus.peopleinspace.navigation.IssPosition -import com.surrus.peopleinspace.navigation.PersonList -import com.surrus.peopleinspace.navigation.TopLevelDestination - - -@Composable -fun rememberPeopleInSpaceAppState( - windowSizeClass: WindowSizeClass -): PeopleInSpaceAppState { - return remember(windowSizeClass) { - PeopleInSpaceAppState(windowSizeClass) - } -} - - -@Stable -class PeopleInSpaceAppState( - private val windowSizeClass: WindowSizeClass -) { - val shouldShowBottomBar: Boolean - get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact || - windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact - - val shouldShowNavRail: Boolean - get() = !shouldShowBottomBar - - /** - * Top level destinations to be used in the BottomBar and NavRail - */ - val topLevelDestinations: List = listOf( - TopLevelDestination( - route = PersonList, - selectedIcon = Icons.Filled.Person, - unselectedIcon = Icons.Outlined.Person, - iconTextId = R.string.people - ), - TopLevelDestination( - route = IssPosition, - selectedIcon = Icons.Filled.LocationOn, - unselectedIcon = Icons.Outlined.LocationOn, - iconTextId = R.string.iss_position - ), - ) -} - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 292915fc..1f1190b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.0.0" ksp = "2.0.0-1.0.21" -compose-multiplatform = "1.6.10" +compose-multiplatform = "1.6.11" composeUiTooling = "1.3.1" coroutines = "1.8.1" kotlinxSerialization = "1.6.3" @@ -18,11 +18,13 @@ sqlDelight = "2.0.2" kmp-nativecoroutines = "1.0.0-ALPHA-31" androidxActivity = "1.9.0" -androidxComposeBom = "2024.05.00" -androidxNavigationCompose = "2.8.0-beta01" -uiToolingPreview = "1.6.7" +androidxComposeBom = "2024.06.00" +material3-adaptive = "1.0.0-alpha12" +material3-adaptive-navigation-suite = "1.0.0-alpha07" +androidxNavigationCompose = "2.8.0-beta03" +uiToolingPreview = "1.6.8" wearCompose = "1.3.1" -androidxLifecycle = "2.8.0" +androidxLifecycle = "2.8.2" imageLoader = "1.7.8" osmdroidAndroid = "6.1.18" @@ -42,7 +44,7 @@ mockito = "3.11.2" multiplatformSettings = "1.2.0" kermit = "2.0.3" -gradleVersionsPlugin = "0.50.0" +gradleVersionsPlugin = "0.51.0" shadowPlugin = "7.0.0" kotlinterGradle = "3.4.5" @@ -82,7 +84,11 @@ androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-compose-material3-WindowSizeClass = { group = "androidx.compose.material3", name="material3-window-size-class" } +androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" } +androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" } +androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" } +androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" } + androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigationCompose" } androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }