This is a post in a series on how I tackled Nav3 concepts and wired things together.
Edit: with 1.1.0-rc01 the impl in this blog post stopped working, or put another way, it wasn’t correctly observing the Lifecycle changes. Fixed now.
There is no Nav3 bottom sheet implementation yet, so this is a quick post on how to create a scene/scene strategy, including the latest “enhancement” from the (yet unreleased) 1.1.0-alpha04 version that fixes the missing animation when popping the back stack.
Of course, there’s also a small hack – my app may open a non-overlay destination from the bottom sheet. That ultimately breaks back handling, as the new destination remains visible while the bottom sheet overlay scene still intercepts the back handling. Therefore, if it is not resumed, we return early. This may slightly affect the animation when returning to the bottom sheet, but it’s almost negligible and a very minor issue compared to the back handling problems.
@OptIn(ExperimentalMaterial3Api::class)
internal class BottomSheetScene<T : Any>(
override val key: Any,
private val entry: NavEntry<T>,
override val previousEntries: List<NavEntry<T>>,
override val overlaidEntries: List<NavEntry<T>>,
private val onBack: () -> Unit,
) : OverlayScene<T> {
private var sheetState: SheetState? = null
override val entries: List<NavEntry<T>> = listOf(entry)
@OptIn(ExperimentalMaterial3Api::class)
override val content: @Composable (() -> Unit) = content@{
val lifecycle = LocalLifecycleOwner.current.lifecycle
// This is a hack for a non-overlay entry on top of bottom-sheet entry.
// We return early to avoid registering ModalBottomSheet prioritized back handling (that would
// reset the back-handling entry going back here).
if (!lifecycle.currentStateAsState().value.isAtLeast(Lifecycle.State.RESUMED)) return@content
ModalBottomSheet(
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
).also {
this.sheetState = it
},
onDismissRequest = onBack,
content = { entry.Content() },
)
DisposableEffect(Unit) {
onDispose { sheetState = null }
}
}
override suspend fun onRemove() {
sheetState?.hide()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as BottomSheetScene<*>
return key == other.key &&
previousEntries == other.previousEntries &&
overlaidEntries == other.overlaidEntries &&
entry == other.entry
}
override fun hashCode(): Int {
return key.hashCode() * 31 +
previousEntries.hashCode() * 31 +
overlaidEntries.hashCode() * 31 +
entry.hashCode() * 31
}
override fun toString(): String {
return "BottomSheetScene(key=$key, entry=$entry, previousEntries=$previousEntries, overlaidEntries=$overlaidEntries)"
}
}
class BottomSheetSceneStrategy<T : Any>() : SceneStrategy<T> {
override fun SceneStrategyScope<T>.calculateScene(
entries: List<NavEntry<T>>,
): Scene<T>? {
val lastEntry = entries.lastOrNull()
val _ = lastEntry?.metadata?.get(BOTTOM_SHEET_KEY) ?: return null
return BottomSheetScene(
key = lastEntry.contentKey,
entry = lastEntry,
previousEntries = entries.dropLast(1),
overlaidEntries = entries.dropLast(1),
onBack = onBack,
)
}
@PublishedApi
internal companion object {
@PublishedApi
internal fun bottomSheet(): Map<String, Any> = mapOf(BOTTOM_SHEET_KEY to Unit)
internal const val BOTTOM_SHEET_KEY = "bottomSheet"
}
}
To use, you may utilize an extension function:
inline fun <reified R : NavKey> EntryProviderScope<NavKey>.entryBottomSheet(noinline content: @Composable (R) -> Unit) =
entry<R>(metadata = BottomSheetSceneStrategy.bottomSheet(), content = content)