Skip to content

Commit fe3bf10

Browse files
tiwizdturner
andauthored
Remove TwoPaneScene in favor of List-Detail (#681)
* Remove TwoPaneScene in favor of List-Detail * Update compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt Co-authored-by: Don Turner <[email protected]> * Update compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt Co-authored-by: Don Turner <[email protected]> * Update compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt Co-authored-by: Don Turner <[email protected]> * Apply Spotless * Add List placeholder * Apply Spotless * Update compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt Co-authored-by: Don Turner <[email protected]> * Fix latest comments * Apply Spotless * Apply suggestion from @dturner * Update the SceneStrategy to own the metadata and helper functions --------- Co-authored-by: Don Turner <[email protected]> Co-authored-by: tiwiz <[email protected]>
1 parent cfce763 commit fe3bf10

File tree

1 file changed

+79
-81
lines changed
  • compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes

1 file changed

+79
-81
lines changed

compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt

Lines changed: 79 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ package com.example.compose.snippets.navigation3.scenes
1919
import androidx.compose.foundation.layout.Column
2020
import androidx.compose.foundation.layout.Row
2121
import androidx.compose.foundation.layout.fillMaxSize
22-
import androidx.compose.material3.Button
23-
import androidx.compose.material3.Text
22+
import androidx.compose.material.Text
2423
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
2524
import androidx.compose.runtime.Composable
2625
import androidx.compose.runtime.remember
2726
import androidx.compose.ui.Modifier
27+
import androidx.navigation3.runtime.NavBackStack
2828
import androidx.navigation3.runtime.NavEntry
2929
import androidx.navigation3.runtime.NavKey
3030
import androidx.navigation3.runtime.entryProvider
@@ -35,6 +35,7 @@ import androidx.navigation3.scene.SceneStrategyScope
3535
import androidx.navigation3.ui.NavDisplay
3636
import androidx.window.core.layout.WindowSizeClass
3737
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
38+
import com.example.compose.snippets.touchinput.Button
3839
import kotlinx.serialization.Serializable
3940

4041
interface SceneExample<T : Any> {
@@ -73,132 +74,129 @@ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> {
7374
// [END android_compose_navigation3_scenes_2]
7475

7576
// [START android_compose_navigation3_scenes_3]
76-
// --- TwoPaneScene ---
77+
// --- ListDetailScene ---
7778
/**
78-
* A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split.
79+
* A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split.
80+
*
7981
*/
80-
class TwoPaneScene<T : Any>(
82+
class ListDetailScene<T : Any>(
8183
override val key: Any,
8284
override val previousEntries: List<NavEntry<T>>,
83-
val firstEntry: NavEntry<T>,
84-
val secondEntry: NavEntry<T>
85+
val listEntry: NavEntry<T>,
86+
val detailEntry: NavEntry<T>,
8587
) : Scene<T> {
86-
override val entries: List<NavEntry<T>> = listOf(firstEntry, secondEntry)
88+
override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry)
8789
override val content: @Composable (() -> Unit) = {
8890
Row(modifier = Modifier.fillMaxSize()) {
89-
Column(modifier = Modifier.weight(0.5f)) {
90-
firstEntry.Content()
91+
Column(modifier = Modifier.weight(0.4f)) {
92+
listEntry.Content()
9193
}
92-
Column(modifier = Modifier.weight(0.5f)) {
93-
secondEntry.Content()
94+
Column(modifier = Modifier.weight(0.6f)) {
95+
detailEntry.Content()
9496
}
9597
}
9698
}
97-
98-
companion object {
99-
internal const val TWO_PANE_KEY = "TwoPane"
100-
/**
101-
* Helper function to add metadata to a [NavEntry] indicating it can be displayed
102-
* in a two-pane layout.
103-
*/
104-
fun twoPane() = mapOf(TWO_PANE_KEY to true)
105-
}
10699
}
107100

108101
@Composable
109-
fun <T : Any> rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy<T> {
102+
fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> {
110103
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
111104

112105
return remember(windowSizeClass) {
113-
TwoPaneSceneStrategy(windowSizeClass)
106+
ListDetailSceneStrategy(windowSizeClass)
114107
}
115108
}
116109

117-
// --- TwoPaneSceneStrategy ---
110+
// --- ListDetailSceneStrategy ---
118111
/**
119-
* A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough
120-
* and the top two back stack entries declare support for two-pane display.
112+
* A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item
113+
* is the backstack is a detail, and before it, at any point in the backstack is a list.
121114
*/
122-
class TwoPaneSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> {
115+
class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> {
116+
123117
override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? {
124-
// Condition 1: Only return a Scene if the window is sufficiently wide to render two panes.
125-
// We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp).
118+
126119
if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
127120
return null
128121
}
129122

130-
val lastTwoEntries = entries.takeLast(2)
131-
132-
// Condition 2: Only return a Scene if there are two entries, and both have declared
133-
// they can be displayed in a two pane scene.
134-
return if (lastTwoEntries.size == 2 &&
135-
lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) }
136-
) {
137-
val firstEntry = lastTwoEntries.first()
138-
val secondEntry = lastTwoEntries.last()
139-
140-
// The scene key must uniquely represent the state of the scene.
141-
val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey)
142-
143-
TwoPaneScene(
144-
key = sceneKey,
145-
// Where we go back to is a UX decision. In this case, we only remove the top
146-
// entry from the back stack, despite displaying two entries in this scene.
147-
// This is because in this app we only ever add one entry to the
148-
// back stack at a time. It would therefore be confusing to the user to add one
149-
// when navigating forward, but remove two when navigating back.
150-
previousEntries = entries.dropLast(1),
151-
firstEntry = firstEntry,
152-
secondEntry = secondEntry
153-
)
154-
} else {
155-
null
156-
}
123+
val detailEntry =
124+
entries.lastOrNull()?.takeIf { it.metadata.containsKey(DETAIL_KEY) } ?: return null
125+
val listEntry = entries.findLast { it.metadata.containsKey(LIST_KEY) } ?: return null
126+
127+
// We use the list's contentKey to uniquely identify the scene.
128+
// This allows the detail panes to be displayed instantly through recomposition, rather than
129+
// having NavDisplay animate the whole scene out when the selected detail item changes.
130+
val sceneKey = listEntry.contentKey
131+
132+
return ListDetailScene(
133+
key = sceneKey,
134+
previousEntries = entries.dropLast(1),
135+
listEntry = listEntry,
136+
detailEntry = detailEntry
137+
)
138+
}
139+
140+
companion object {
141+
internal const val LIST_KEY = "ListDetailScene-List"
142+
internal const val DETAIL_KEY = "ListDetailScene-Detail"
143+
144+
/**
145+
* Helper function to add metadata to a [NavEntry] indicating it can be displayed
146+
* as a list in the [ListDetailScene].
147+
*/
148+
fun listPane() = mapOf(LIST_KEY to true)
149+
150+
/**
151+
* Helper function to add metadata to a [NavEntry] indicating it can be displayed
152+
* as a list in the [ListDetailScene].
153+
*/
154+
fun detailPane() = mapOf(DETAIL_KEY to true)
157155
}
158156
}
159157
// [END android_compose_navigation3_scenes_3]
160158

161159
// [START android_compose_navigation3_scenes_4]
162160
// Define your navigation keys
163161
@Serializable
164-
data object ProductList : NavKey
162+
data object ConversationList : NavKey
163+
165164
@Serializable
166-
data class ProductDetail(val id: String) : NavKey
165+
data class ConversationDetail(val id: String) : NavKey
167166

168167
@Composable
169168
fun MyAppContent() {
170-
val backStack = rememberNavBackStack(ProductList)
169+
val backStack = rememberNavBackStack(ConversationList)
170+
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()
171171

172172
NavDisplay(
173173
backStack = backStack,
174+
onBack = { backStack.removeLastOrNull() },
175+
sceneStrategy = listDetailStrategy,
174176
entryProvider = entryProvider {
175-
entry<ProductList>(
176-
// Mark this entry as eligible for two-pane display
177-
metadata = TwoPaneScene.twoPane()
178-
) { key ->
179-
Column {
180-
Text("Product List")
181-
Button(onClick = { backStack.add(ProductDetail("ABC")) }) {
182-
Text("View Details for ABC (Two-Pane Eligible)")
177+
entry<ConversationList>(
178+
metadata = ListDetailSceneStrategy.listPane()
179+
) {
180+
Column(modifier = Modifier.fillMaxSize()) {
181+
Text(text = "I'm a Conversation List")
182+
Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) {
183+
Text(text = "Open detail")
183184
}
184185
}
185186
}
186-
187-
entry<ProductDetail>(
188-
// Mark this entry as eligible for two-pane display
189-
metadata = TwoPaneScene.twoPane()
190-
) { key ->
191-
Text("Product Detail: ${key.id} (Two-Pane Eligible)")
192-
}
193-
// ... other entries ...
194-
},
195-
// Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically.
196-
sceneStrategy = rememberTwoPaneSceneStrategy(),
197-
onBack = {
198-
if (backStack.isNotEmpty()) {
199-
backStack.removeLastOrNull()
187+
entry<ConversationDetail>(
188+
metadata = ListDetailSceneStrategy.detailPane()
189+
) {
190+
Text(text = "I'm a Conversation Detail")
200191
}
201192
}
202193
)
203194
}
195+
196+
private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) {
197+
198+
// Remove any existing detail routes, then add the new detail route
199+
removeIf { it is ConversationDetail }
200+
add(detailRoute)
201+
}
204202
// [END android_compose_navigation3_scenes_4]

0 commit comments

Comments
 (0)