@@ -19,12 +19,12 @@ package com.example.compose.snippets.navigation3.scenes
1919import androidx.compose.foundation.layout.Column
2020import androidx.compose.foundation.layout.Row
2121import androidx.compose.foundation.layout.fillMaxSize
22- import androidx.compose.material3.Button
23- import androidx.compose.material3.Text
22+ import androidx.compose.material.Text
2423import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
2524import androidx.compose.runtime.Composable
2625import androidx.compose.runtime.remember
2726import androidx.compose.ui.Modifier
27+ import androidx.navigation3.runtime.NavBackStack
2828import androidx.navigation3.runtime.NavEntry
2929import androidx.navigation3.runtime.NavKey
3030import androidx.navigation3.runtime.entryProvider
@@ -35,6 +35,7 @@ import androidx.navigation3.scene.SceneStrategyScope
3535import androidx.navigation3.ui.NavDisplay
3636import androidx.window.core.layout.WindowSizeClass
3737import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
38+ import com.example.compose.snippets.touchinput.Button
3839import kotlinx.serialization.Serializable
3940
4041interface 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
169168fun 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