Navigation 3 - Part 2 - Dual Pane Examples
Part 2
I was originally going to stick with two parts for this guide, but there was just far too much content to cram it in. I think the final version would have introduce far too many concepts and this would have been a nightmare to explain and learn from.
Rather than jump straight from the basic introduction to the final muscle car example, I decided to add an intermediate step. So here we are, the added part two.
Focus
This part will focus on adding dual-pane support and and a backstack guard to handle that (our previous version will not). We will briefly cover size restrictions for dual-pane testing as well as hierarchical back vs chronological back.
Dual Pane Example 1
NavigationApp
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun NavigationApp() {
val backStack =
rememberNavBackStack(NavigationScreen.Home)
val listDetailStrategy =
rememberListDetailSceneStrategy<NavKey>()
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
transitionSpec = TransitionAnimations
.forwardTransition,
popTransitionSpec = TransitionAnimations
.popTransition,
predictivePopTransitionSpec = TransitionAnimations
.predictivePopTransitionSpec,
sceneStrategies = listOf(listDetailStrategy),
entryProvider = entryProvider<NavKey> {
entry<NavigationScreen.Home> {
GuardedNavigateDualPane(backStack) {
HomeScreen()
}
}
entry<NavigationScreen.List>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder() }
)
) {
GuardedNavigateDualPane(backStack) {
ListScreen()
}
}
entry<NavigationScreen.Detail>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
GuardedNavigateDualPane(backStack) {
BasicNav1.DetailScreen(key.id)
}
}
}
)
}
DetailPlaceholder
This is the screen that gets displayed if we are in dual pane and no item from our list has been displayed.
When testing, make sure you have an expanded screen. I used Pixel 9a emulator and also tested on a tablet layout.
@Composable
private fun DetailPlaceholder() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
"Select something",
style = MaterialTheme.typography.bodyLarge
)
}
}
GuardedNavigateDualPane
Finally, we need to update GuardedNavigate as our previous solution does not work with dual pane.
Previous Solution
Our previous solution used guardedNavLambda to check the calling screen was at the end of the stack before adding the new entry. If you clicked more than once, it would only accept the first click as the backstack would then change to hold the selected details.
val guardedNavLambda = remember {
{ screen: NavigationScreen ->
if (backStack.lastOrNull() == key)
backStack.add(screen)
}
}
This was safe for single pane as you could never select from the list when a detail screen was being displayed, but that obviously doesn't work for dual pane. It was a good solution for the problem, but it didn't scale.
New Solution
The new solution does things a little differently as we now need to allow transitions when a detail screen is already displayed.
Navigation from list to detail will always fire, but instead of removing and adding the last element, it checks to see if the same screen type (in our case the detail) already exists and replaces it with the new entry.
We are using it::class == screen::class to check if we already have the same type in the list rather than comparing ids which can be different for the same class type. This ensures we only have one of each type in our backstack.
If there are no screens of the current type in the list, it simply adds the selection to the backstack.
This leads us nicely to hierarchical back vs chronological back. From the names, you may be able to tell hierarchical relates to structure (layers) and chronological relates to time. For this app, we are using hierarchical back. Chronological back would be better suited when the user needs a history of the pages they viewed.
Also of note, we no longer need to pass the key into the function. We add entries with a simple GuardedNavigateDualPane(backStack) { screen }.
@Composable
private fun GuardedNavigateDualPane(
backStack: NavBackStack<NavKey>,
content: @Composable () -> Unit
) {
val guardedNavLambda: (NavigationScreen) -> Unit =
remember(backStack) {
{ screen: NavigationScreen ->
val existing = backStack.indexOfLast {
it::class == screen::class
}
if (existing >= 0) {
backStack[existing] = screen
} else {
backStack.add(screen)
}
}
}
...
}
Working
Dual Pane Example 2
State Holder
@Composable
fun rememberDualPaneNavLambda(
backStack: NavBackStack<NavKey>
): (NavigationScreen) -> Unit =
remember(backStack) {
{ screen: NavigationScreen ->
val existing = backStack
.indexOfLast { it::class == screen::class }
if (existing >= 0) {
backStack[existing] = screen
} else {
backStack.add(screen)
}
}
}
Final NavigationApp
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun NavigationApp() {
val backStack =
rememberNavBackStack(NavigationScreen.Home)
val guardedNavLambda = rememberDualPaneNavLambda(backStack)
val listDetailStrategy =
rememberListDetailSceneStrategy<NavKey>()
CompositionLocalProvider(
LocalNavigate provides guardedNavLambda
) {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
transitionSpec = TransitionAnimations
.forwardTransition,
popTransitionSpec = TransitionAnimations
.popTransition,
predictivePopTransitionSpec = TransitionAnimations
.predictivePopTransitionSpec,
sceneStrategies = listOf(listDetailStrategy),
entryProvider = entryProvider<NavKey> {
entry<NavigationScreen.Home> { HomeScreen() }
entry<NavigationScreen.List>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = {
DetailPlaceholder()
}
)
) { ListScreen() }
entry<NavigationScreen.Detail>(
metadata =
ListDetailSceneStrategy.detailPane()
) { key -> BasicNav1.DetailScreen(key.id) }
}
)
}
}
Summary
So we now have smooth, Process Death safe, dual pane navigation using the new Navigation3 library. I think that's something to celebrate!
Next Up - Muscle Car App
I will likely need a little time to get this done, but Part 3 of this series will be the Muscle Car App with viewModel support. Again, I will make sure it is Process Death safe!
Links
My links
GitHub Gists
Related Blog Posts
This Series
- Navigation 3 Part 1 - Basic Examples
- Navigation 3 Part 2 - Dual Pane Examples - This
- Navigation 3 Part 3 - Muscle Car App
- Navigation 3 Part 1, 2 and 3 Dependencies (Shared)
- Navigation 3 Part 1 - Buggy Old Version
Other Related
Official links
Google:
- Navigation3
- Material 3 Layouts
- Nav3 Recipes (GitHub)
- Nav3 Recipes (GitHub) - Passing ViewModels
- State Holders
- Backstack
- CompositionLocal
- ViewModel
- StateFlow and SharedFlow
- Lifecycle
- Recomposition
.png)


Comments
Post a Comment