Skip to content

Commit 4457a93

Browse files
jbw0033dturnertiwizianhanniballakegemini-code-assist[bot]
authored
Add bottom sheet recipe (#67)
* Adding tests for Navigator * First draft of Navigator for nested and shared destinations * Address AI feedback * Add Nav2 activity and dependencies * Add starting point for migration * Step 1 of migration complete * Lots of refactoring * Add Step 4 of the migration * Add Step 5 of the migration * Part way through step 6 of migration * Updating all steps * Add final step 7. * Tidy up imports * Navigator uses SavedState APIs and KotlinX Serialization to persist state through config changes * Remove remaining Nav2 references from step 7 * Handle shared routes * Switch to using Saver * Add bottom sheet recipe This commit adds a recipe that demonstrates how to display a navigation destination within a Material `ModalBottomSheet`. It introduces `BottomSheetSceneStrategy`, a `SceneStrategy` that can be added to a `NavDisplay`. This strategy checks for specific metadata on a `NavEntry` to determine if it should be rendered as a bottom sheet. A new `BottomSheetActivity` is added to showcase how to use this strategy. * Remove parenthesis from BottomSheetSceneStrategy Doing this for Kotlin code syntax * Minor edits * Update headings in README for better structure * Update to latest library versions and disable snapshot artifact repo * Replace SnapshotStateList with NavBackStack * Address Gemini feedback * Add link to bug * Create Supporting Pane Recipe * Create Supporting Pane Recipe * Update dependencies to sync with Supporting Pane PR * Fix comments * Fix comments * Update README file * Fix naming * Add koin injected ViewModel recipe * Slight formatting change * Addressing AI code review * Move Material recipes to their specific package * Update README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update README.md * Update README.md * Update README.md * Update README.md * Add bottom sheet recipe This commit adds a recipe that demonstrates how to display a navigation destination within a Material `ModalBottomSheet`. It introduces `BottomSheetSceneStrategy`, a `SceneStrategy` that can be added to a `NavDisplay`. This strategy checks for specific metadata on a `NavEntry` to determine if it should be rendered as a bottom sheet. A new `BottomSheetActivity` is added to showcase how to use this strategy. * Remove parenthesis from BottomSheetSceneStrategy Doing this for Kotlin code syntax * Minor edits * Add bottom sheet recipe This commit adds a recipe that demonstrates how to display a navigation destination within a Material `ModalBottomSheet`. It introduces `BottomSheetSceneStrategy`, a `SceneStrategy` that can be added to a `NavDisplay`. This strategy checks for specific metadata on a `NavEntry` to determine if it should be rendered as a bottom sheet. A new `BottomSheetActivity` is added to showcase how to use this strategy. * Remove parenthesis from BottomSheetSceneStrategy Doing this for Kotlin code syntax * Minor edits * Update to alpha10 * Update gradle/libs.versions.toml Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Add migration guide * Update guide * Update guide * Update guide * Update guide * Update guide * Update guide * Update guide * Remove navigator package * Fix errors in Bottomsheet Recipe Fix imports and remove extra name attribute in manifest. * Refactor to render NavDisplay on top of NavHost, rather than wrapping it * Update to use alpha10 API * Update links in migration guide * Fix link to migration package * Fix version --------- Co-authored-by: Don Turner <[email protected]> Co-authored-by: Don Turner <[email protected]> Co-authored-by: Rob Orgiu <[email protected]> Co-authored-by: Ian Lake <[email protected]> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 71785e1 commit 4457a93

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565
android:name=".dialog.DialogActivity"
6666
android:exported="true"
6767
android:theme="@style/Theme.Nav3Recipes"/>
68+
<activity
69+
android:name=".bottomsheet.BottomSheetActivity"
70+
android:exported="true"
71+
android:theme="@style/Theme.Nav3Recipes"/>
6872
<activity
6973
android:name=".material.listdetail.MaterialListDetailActivity"
7074
android:exported="true"

app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import com.example.nav3recipes.animations.AnimatedActivity
2929
import com.example.nav3recipes.basic.BasicActivity
3030
import com.example.nav3recipes.basicdsl.BasicDslActivity
3131
import com.example.nav3recipes.basicsaveable.BasicSaveableActivity
32+
import com.example.nav3recipes.bottomsheet.BottomSheetActivity
3233
import com.example.nav3recipes.commonui.CommonUiActivity
3334
import com.example.nav3recipes.conditional.ConditionalActivity
3435
import com.example.nav3recipes.dialog.DialogActivity
@@ -60,9 +61,11 @@ private val recipes = listOf(
6061
Recipe("Basic Saveable", BasicSaveableActivity::class.java),
6162

6263
Heading("Layouts and animations"),
64+
Recipe("Bottom Sheet", BottomSheetActivity::class.java),
6365
Recipe("Material list-detail layout", MaterialListDetailActivity::class.java),
6466
Recipe("Material supporting-pane layout", MaterialSupportingPaneActivity::class.java),
6567
Recipe("Dialog", DialogActivity::class.java),
68+
Recipe("Material list-detail layout", MaterialListDetailActivity::class.java),
6669
Recipe("Two pane layout (custom scene)", TwoPaneActivity::class.java),
6770
Recipe("Animations", AnimatedActivity::class.java),
6871

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.nav3recipes.bottomsheet
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.compose.foundation.shape.RoundedCornerShape
23+
import androidx.compose.material3.Button
24+
import androidx.compose.material3.ExperimentalMaterial3Api
25+
import androidx.compose.material3.Text
26+
import androidx.compose.runtime.remember
27+
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.draw.clip
29+
import androidx.compose.ui.unit.dp
30+
import androidx.navigation3.runtime.NavKey
31+
import androidx.navigation3.runtime.entryProvider
32+
import androidx.navigation3.runtime.rememberNavBackStack
33+
import androidx.navigation3.ui.NavDisplay
34+
import com.example.nav3recipes.content.ContentBlue
35+
import com.example.nav3recipes.content.ContentGreen
36+
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
37+
import kotlinx.serialization.Serializable
38+
39+
/**
40+
* This recipe demonstrates how to create a bottom sheet. It does this by:
41+
*
42+
* - Adding the `BottomSheetSceneStrategy` to the list of strategies used by `NavDisplay`.
43+
* - Adding `BottomSheetSceneStrategy.bottomSheet()` to a `NavEntry`'s metadata to indicate that it
44+
* is a bottom sheet. In this case it is applied to the `NavEntry` for `RouteB`.
45+
*
46+
* See also https://developer.android.com/guide/navigation/navigation-3/custom-layouts
47+
*/
48+
49+
@Serializable
50+
private data object RouteA : NavKey
51+
52+
@Serializable
53+
private data class RouteB(val id: String) : NavKey
54+
55+
class BottomSheetActivity : ComponentActivity() {
56+
57+
@OptIn(ExperimentalMaterial3Api::class)
58+
override fun onCreate(savedInstanceState: Bundle?) {
59+
setEdgeToEdgeConfig()
60+
super.onCreate(savedInstanceState)
61+
setContent {
62+
val backStack = rememberNavBackStack(RouteA)
63+
val bottomSheetStrategy = remember { BottomSheetSceneStrategy<NavKey>() }
64+
65+
NavDisplay(
66+
backStack = backStack,
67+
onBack = { backStack.removeLastOrNull() },
68+
sceneStrategy = bottomSheetStrategy,
69+
entryProvider = entryProvider {
70+
entry<RouteA> {
71+
ContentGreen("Welcome to Nav3") {
72+
Button(onClick = {
73+
backStack.add(RouteB("123"))
74+
}) {
75+
Text("Click to open bottom sheet")
76+
}
77+
}
78+
}
79+
entry<RouteB>(
80+
metadata = BottomSheetSceneStrategy.bottomSheet()
81+
) { key ->
82+
ContentBlue(
83+
title = "Route id: ${key.id}",
84+
modifier = Modifier.clip(
85+
shape = RoundedCornerShape(16.dp)
86+
)
87+
)
88+
}
89+
}
90+
)
91+
}
92+
}
93+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.example.nav3recipes.bottomsheet
2+
3+
import androidx.compose.material3.ExperimentalMaterial3Api
4+
import androidx.compose.material3.ModalBottomSheet
5+
import androidx.compose.material3.ModalBottomSheetProperties
6+
import androidx.compose.runtime.Composable
7+
import androidx.navigation3.runtime.NavEntry
8+
import androidx.navigation3.scene.OverlayScene
9+
import androidx.navigation3.scene.Scene
10+
import androidx.navigation3.scene.SceneStrategy
11+
12+
/** An [OverlayScene] that renders an [entry] within a [ModalBottomSheet]. */
13+
@OptIn(ExperimentalMaterial3Api::class)
14+
internal class BottomSheetScene<T : Any>(
15+
override val key: T,
16+
override val previousEntries: List<NavEntry<T>>,
17+
override val overlaidEntries: List<NavEntry<T>>,
18+
private val entry: NavEntry<T>,
19+
private val modalBottomSheetProperties: ModalBottomSheetProperties,
20+
private val onBack: (count: Int) -> Unit,
21+
) : OverlayScene<T> {
22+
23+
override val entries: List<NavEntry<T>> = listOf(entry)
24+
25+
override val content: @Composable (() -> Unit) = {
26+
ModalBottomSheet(
27+
onDismissRequest = { onBack(1) },
28+
properties = modalBottomSheetProperties,
29+
) {
30+
entry.Content()
31+
}
32+
}
33+
}
34+
35+
/**
36+
* A [SceneStrategy] that displays entries that have added [bottomSheet] to their [NavEntry.metadata]
37+
* within a [ModalBottomSheet] instance.
38+
*
39+
* This strategy should always be added before any non-overlay scene strategies.
40+
*/
41+
@OptIn(ExperimentalMaterial3Api::class)
42+
class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> {
43+
44+
@Composable
45+
override fun calculateScene(
46+
entries: List<NavEntry<T>>,
47+
onBack: (Int) -> Unit
48+
): Scene<T>? {
49+
val lastEntry = entries.lastOrNull()
50+
val bottomSheetProperties = lastEntry?.metadata?.get(BOTTOM_SHEET_KEY) as? ModalBottomSheetProperties
51+
return bottomSheetProperties?.let { properties ->
52+
@Suppress("UNCHECKED_CAST")
53+
BottomSheetScene(
54+
key = lastEntry.contentKey as T,
55+
previousEntries = entries.dropLast(1),
56+
overlaidEntries = entries.dropLast(1),
57+
entry = lastEntry,
58+
modalBottomSheetProperties = properties,
59+
onBack = onBack
60+
)
61+
}
62+
}
63+
64+
companion object {
65+
/**
66+
* Function to be called on the [NavEntry.metadata] to mark this entry as something that
67+
* should be displayed within a [ModalBottomSheet].
68+
*
69+
* @param modalBottomSheetProperties properties that should be passed to the containing
70+
* [ModalBottomSheet].
71+
*/
72+
@OptIn(ExperimentalMaterial3Api::class)
73+
fun bottomSheet(
74+
modalBottomSheetProperties: ModalBottomSheetProperties = ModalBottomSheetProperties()
75+
): Map<String, Any> = mapOf(BOTTOM_SHEET_KEY to modalBottomSheetProperties)
76+
77+
internal const val BOTTOM_SHEET_KEY = "bottomsheet"
78+
}
79+
}

0 commit comments

Comments
 (0)