From 7c60e68a4bfd3078b12aca75a7aaed1fc7eb671f Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Tue, 20 Aug 2024 12:04:38 +0200 Subject: [PATCH] Commonize "material-navigation" module. --- .../api/desktop/material-navigation.api | 44 ++++ .../material/material-navigation/build.gradle | 77 +++++-- .../navigation/BottomSheetNavigatorTest.kt | 0 .../navigation/NavGraphBuilderTest.kt | 0 .../navigation/SheetContentHostTest.kt | 0 .../BottomSheetNavigator.android.kt} | 59 +----- .../material/navigation/BottomSheet.kt | 0 .../navigation/BottomSheetNavigator.kt | 126 +++++++++++ .../material/navigation/NavGraphBuilder.kt | 0 .../material/navigation/SheetContentHost.kt | 0 .../navigation/BottomSheetNavigator.jb.kt | 200 ++++++++++++++++++ gradle.properties | 2 + mpp/build.gradle.kts | 1 + settings.gradle | 1 + 14 files changed, 443 insertions(+), 67 deletions(-) create mode 100644 compose/material/material-navigation/api/desktop/material-navigation.api rename compose/material/material-navigation/src/{androidTest/java => androidInstrumentedTest/kotlin}/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt (100%) rename compose/material/material-navigation/src/{androidTest/java => androidInstrumentedTest/kotlin}/androidx/compose/material/navigation/NavGraphBuilderTest.kt (100%) rename compose/material/material-navigation/src/{androidTest/java => androidInstrumentedTest/kotlin}/androidx/compose/material/navigation/SheetContentHostTest.kt (100%) rename compose/material/material-navigation/src/{main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt => androidMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.android.kt} (80%) rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/BottomSheet.kt (100%) create mode 100644 compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.kt rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/NavGraphBuilder.kt (100%) rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/SheetContentHost.kt (100%) create mode 100644 compose/material/material-navigation/src/jbMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.jb.kt diff --git a/compose/material/material-navigation/api/desktop/material-navigation.api b/compose/material/material-navigation/api/desktop/material-navigation.api new file mode 100644 index 0000000000000..702b3a11339ff --- /dev/null +++ b/compose/material/material-navigation/api/desktop/material-navigation.api @@ -0,0 +1,44 @@ +public final class androidx/compose/material/navigation/BottomSheetKt { + public static final fun ModalBottomSheetLayout-4erKP6g (Landroidx/compose/material/navigation/BottomSheetNavigator;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;FJJJLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +} + +public final class androidx/compose/material/navigation/BottomSheetNavigator : androidx/navigation/Navigator { + public static final field $stable I + public fun (Landroidx/compose/material/ModalBottomSheetState;)V + public fun createDestination ()Landroidx/compose/material/navigation/BottomSheetNavigator$Destination; + public synthetic fun createDestination ()Landroidx/navigation/NavDestination; + public final fun getNavigatorSheetState ()Landroidx/compose/material/navigation/BottomSheetNavigatorSheetState; + public fun navigate (Ljava/util/List;Landroidx/navigation/NavOptions;Landroidx/navigation/Navigator$Extras;)V + public fun onAttach (Landroidx/navigation/NavigatorState;)V + public fun popBackStack (Landroidx/navigation/NavBackStackEntry;Z)V +} + +public final class androidx/compose/material/navigation/BottomSheetNavigator$Destination : androidx/navigation/NavDestination, androidx/navigation/FloatingWindow { + public static final field $stable I + public fun (Landroidx/compose/material/navigation/BottomSheetNavigator;Lkotlin/jvm/functions/Function4;)V +} + +public final class androidx/compose/material/navigation/BottomSheetNavigatorKt { + public static final fun rememberBottomSheetNavigator (Landroidx/compose/animation/core/AnimationSpec;Landroidx/compose/runtime/Composer;II)Landroidx/compose/material/navigation/BottomSheetNavigator; +} + +public final class androidx/compose/material/navigation/BottomSheetNavigatorSheetState { + public static final field $stable I + public fun (Landroidx/compose/material/ModalBottomSheetState;)V + public final fun getCurrentValue ()Landroidx/compose/material/ModalBottomSheetValue; + public final fun getTargetValue ()Landroidx/compose/material/ModalBottomSheetValue; + public final fun isVisible ()Z +} + +public final class androidx/compose/material/navigation/ComposableSingletons$BottomSheetNavigator_jbKt { + public static final field INSTANCE Landroidx/compose/material/navigation/ComposableSingletons$BottomSheetNavigator_jbKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$material_navigation ()Lkotlin/jvm/functions/Function4; +} + +public final class androidx/compose/material/navigation/NavGraphBuilderKt { + public static final fun bottomSheet (Landroidx/navigation/NavGraphBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function4;)V + public static synthetic fun bottomSheet$default (Landroidx/navigation/NavGraphBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)V +} + diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle index 1a55ed7c3e994..532f7b0dddfde 100644 --- a/compose/material/material-navigation/build.gradle +++ b/compose/material/material-navigation/build.gradle @@ -13,37 +13,82 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import androidx.build.AndroidXComposePlugin +import androidx.build.JetbrainsAndroidXPlugin import androidx.build.LibraryType +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType plugins { id("AndroidXPlugin") id("com.android.library") id("AndroidXComposePlugin") - id("org.jetbrains.kotlin.android") + id("JetbrainsAndroidXPlugin") +} + +AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project) +JetbrainsAndroidXPlugin.applyAndConfigure(project) + +androidXComposeMultiplatform { + android() + desktop() + darwin() + js() + wasm() } -dependencies { - api("androidx.navigation:navigation-compose:2.7.7") - implementation(project(":compose:material:material")) - implementation(libs.kotlinStdlib) - - androidTestImplementation project(":compose:test-utils") - androidTestImplementation("androidx.navigation:navigation-testing:2.7.7") - androidTestImplementation(project(":compose:ui:ui-test-junit4")) - androidTestImplementation(project(":compose:ui:ui-test-manifest")) - androidTestImplementation(libs.testRunner) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.truth) - androidTestImplementation(libs.testRules) +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":navigation:navigation-compose")) + implementation(project(":compose:material:material")) + implementation(libs.kotlinStdlib) + } + } + + androidInstrumentedTest { + dependencies { + implementation(project(":compose:test-utils")) + implementation(project(":navigation:navigation-testing")) + implementation(project(":compose:ui:ui-test-junit4")) + implementation(project(":compose:ui:ui-test-manifest")) + implementation(libs.testRunner) + implementation(libs.junit) + implementation(libs.truth) + implementation(libs.testRules) + } + } + + jbMain.dependsOn(commonMain) + desktopMain.dependsOn(jbMain) + nativeMain.dependsOn(jbMain) + webMain.dependsOn(jbMain) + + targets.all { target -> + if (target.platformType == KotlinPlatformType.native) { + target.compilations["main"].defaultSourceSet { + dependsOn(nativeMain) + } + } else if (target.platformType in [ + KotlinPlatformType.js, + KotlinPlatformType.wasm + ]) { + target.compilations["main"].defaultSourceSet { + dependsOn(webMain) + } + } + } + } } androidx { name = "Compose Material Navigation" - type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS + type = LibraryType.PUBLISHED_LIBRARY mavenVersion = LibraryVersions.COMPOSE inceptionYear = "2024" description = "Compose Material integration with Navigation" - samples(projectOrArtifact(":compose:material:material-navigation-samples")) +// samples(projectOrArtifact(":compose:material:material-navigation-samples")) } android { diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt b/compose/material/material-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt similarity index 100% rename from compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt rename to compose/material/material-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/NavGraphBuilderTest.kt b/compose/material/material-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material/navigation/NavGraphBuilderTest.kt similarity index 100% rename from compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/NavGraphBuilderTest.kt rename to compose/material/material-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material/navigation/NavGraphBuilderTest.kt diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/SheetContentHostTest.kt b/compose/material/material-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material/navigation/SheetContentHostTest.kt similarity index 100% rename from compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/SheetContentHostTest.kt rename to compose/material/material-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material/navigation/SheetContentHostTest.kt diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt b/compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.android.kt similarity index 80% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt rename to compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.android.kt index 92a2db19f010c..5a769fecb9927 100644 --- a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt +++ b/compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.android.kt @@ -17,19 +17,15 @@ package androidx.compose.material.navigation import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material.navigation.BottomSheetNavigator.Destination import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue import androidx.compose.ui.util.fastForEach @@ -44,45 +40,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.transform -/** - * The state of a [ModalBottomSheetLayout] that the [BottomSheetNavigator] drives - * - * @param sheetState The sheet state that is driven by the [BottomSheetNavigator] - */ -public class BottomSheetNavigatorSheetState(private val sheetState: ModalBottomSheetState) { - /** - * @see ModalBottomSheetState.isVisible - */ - public val isVisible: Boolean - get() = sheetState.isVisible - - /** - * @see ModalBottomSheetState.currentValue - */ - public val currentValue: ModalBottomSheetValue - get() = sheetState.currentValue - - /** - * @see ModalBottomSheetState.targetValue - */ - public val targetValue: ModalBottomSheetValue - get() = sheetState.targetValue -} - -/** - * Create and remember a [BottomSheetNavigator] - */ -@Composable -public fun rememberBottomSheetNavigator( - animationSpec: AnimationSpec = SpringSpec() -): BottomSheetNavigator { - val sheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.Hidden, - animationSpec = animationSpec - ) - return remember(sheetState) { BottomSheetNavigator(sheetState) } -} - /** * Navigator that drives a [ModalBottomSheetState] for use of [ModalBottomSheetLayout]s * with the navigation library. Every destination using this Navigator must set a valid @@ -103,8 +60,8 @@ public fun rememberBottomSheetNavigator( * drive the sheet state */ @Navigator.Name("bottomSheet") -public class BottomSheetNavigator( - internal val sheetState: ModalBottomSheetState +public actual class BottomSheetNavigator actual constructor( + internal actual val sheetState: ModalBottomSheetState ) : Navigator() { private var attached by mutableStateOf(false) @@ -126,7 +83,7 @@ public class BottomSheetNavigator( * composed before the Navigator is attached, so we specifically return an empty flow if we * aren't attached yet. */ - internal val transitionsInProgress: StateFlow> + internal actual val transitionsInProgress: StateFlow> get() = if (attached) { state.transitionsInProgress } else { @@ -136,14 +93,14 @@ public class BottomSheetNavigator( /** * Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState] */ - public val navigatorSheetState: BottomSheetNavigatorSheetState = + public actual val navigatorSheetState: BottomSheetNavigatorSheetState = BottomSheetNavigatorSheetState(sheetState) /** * A [Composable] function that hosts the current sheet content. This should be set as * sheetContent of your [ModalBottomSheetLayout]. */ - internal val sheetContent: @Composable ColumnScope.() -> Unit = { + internal actual val sheetContent: @Composable ColumnScope.() -> Unit = { val saveableStateHolder = rememberSaveableStateHolder() val transitionsInProgressEntries by transitionsInProgress.collectAsState() @@ -235,8 +192,8 @@ public class BottomSheetNavigator( * [NavDestination] specific to [BottomSheetNavigator] */ @NavDestination.ClassType(Composable::class) - public class Destination( + public actual class Destination actual constructor( navigator: BottomSheetNavigator, - internal val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit + internal actual val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit ) : NavDestination(navigator), FloatingWindow } diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheet.kt similarity index 100% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheet.kt diff --git a/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.kt new file mode 100644 index 0000000000000..e494aea633037 --- /dev/null +++ b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.material.navigation + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.navigation.BottomSheetNavigator.Destination +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.FloatingWindow +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination +import androidx.navigation.Navigator +import androidx.navigation.NavigatorState +import kotlinx.coroutines.flow.StateFlow + +/** + * The state of a [ModalBottomSheetLayout] that the [BottomSheetNavigator] drives + * + * @param sheetState The sheet state that is driven by the [BottomSheetNavigator] + */ +public class BottomSheetNavigatorSheetState(private val sheetState: ModalBottomSheetState) { + /** + * @see ModalBottomSheetState.isVisible + */ + public val isVisible: Boolean + get() = sheetState.isVisible + + /** + * @see ModalBottomSheetState.currentValue + */ + public val currentValue: ModalBottomSheetValue + get() = sheetState.currentValue + + /** + * @see ModalBottomSheetState.targetValue + */ + public val targetValue: ModalBottomSheetValue + get() = sheetState.targetValue +} + +/** + * Create and remember a [BottomSheetNavigator] + */ +@Composable +public fun rememberBottomSheetNavigator( + animationSpec: AnimationSpec = SpringSpec() +): BottomSheetNavigator { + val sheetState = rememberModalBottomSheetState( + ModalBottomSheetValue.Hidden, + animationSpec = animationSpec + ) + return remember(sheetState) { BottomSheetNavigator(sheetState) } +} + +/** + * Navigator that drives a [ModalBottomSheetState] for use of [ModalBottomSheetLayout]s + * with the navigation library. Every destination using this Navigator must set a valid + * [Composable] by setting it directly on an instantiated [Destination] or calling + * [androidx.compose.material.navigation.bottomSheet]. + * + * The [sheetContent] [Composable] will always host the latest entry of the back stack. When + * navigating from a [BottomSheetNavigator.Destination] to another + * [BottomSheetNavigator.Destination], the content of the sheet will be replaced instead of a + * new bottom sheet being shown. + * + * When the sheet is dismissed by the user, the [state]'s [NavigatorState.backStack] will be popped. + * + * The primary constructor is not intended for public use. Please refer to + * [rememberBottomSheetNavigator] instead. + * + * @param sheetState The [ModalBottomSheetState] that the [BottomSheetNavigator] will use to + * drive the sheet state + */ +public expect class BottomSheetNavigator( + sheetState: ModalBottomSheetState +) : Navigator { + + internal val sheetState: ModalBottomSheetState + + /** + * Get the transitionsInProgress from the [state]. In some cases, the [sheetContent] might be + * composed before the Navigator is attached, so we specifically return an empty flow if we + * aren't attached yet. + */ + internal val transitionsInProgress: StateFlow> + + /** + * Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState] + */ + public val navigatorSheetState: BottomSheetNavigatorSheetState + + /** + * A [Composable] function that hosts the current sheet content. This should be set as + * sheetContent of your [ModalBottomSheetLayout]. + */ + internal val sheetContent: @Composable ColumnScope.() -> Unit + + /** + * [NavDestination] specific to [BottomSheetNavigator] + */ + public class Destination( + navigator: BottomSheetNavigator, + content: @Composable ColumnScope.(NavBackStackEntry) -> Unit + ) : NavDestination, FloatingWindow { + internal val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit + } +} diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/NavGraphBuilder.kt similarity index 100% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/NavGraphBuilder.kt diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/SheetContentHost.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/SheetContentHost.kt similarity index 100% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/SheetContentHost.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/SheetContentHost.kt diff --git a/compose/material/material-navigation/src/jbMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.jb.kt b/compose/material/material-navigation/src/jbMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.jb.kt new file mode 100644 index 0000000000000..c7b93a4ff3067 --- /dev/null +++ b/compose/material/material-navigation/src/jbMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.jb.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.material.navigation + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.navigation.BottomSheetNavigator.Destination +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.runtime.setValue +import androidx.compose.ui.util.fastForEach +import androidx.navigation.FloatingWindow +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination +import androidx.navigation.NavOptions +import androidx.navigation.Navigator +import androidx.navigation.NavigatorState +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.transform + +/** + * Navigator that drives a [ModalBottomSheetState] for use of [ModalBottomSheetLayout]s + * with the navigation library. Every destination using this Navigator must set a valid + * [Composable] by setting it directly on an instantiated [Destination] or calling + * [androidx.compose.material.navigation.bottomSheet]. + * + * The [sheetContent] [Composable] will always host the latest entry of the back stack. When + * navigating from a [BottomSheetNavigator.Destination] to another + * [BottomSheetNavigator.Destination], the content of the sheet will be replaced instead of a + * new bottom sheet being shown. + * + * When the sheet is dismissed by the user, the [state]'s [NavigatorState.backStack] will be popped. + * + * The primary constructor is not intended for public use. Please refer to + * [rememberBottomSheetNavigator] instead. + * + * @param sheetState The [ModalBottomSheetState] that the [BottomSheetNavigator] will use to + * drive the sheet state + */ +public actual class BottomSheetNavigator actual constructor( + internal actual val sheetState: ModalBottomSheetState +) : Navigator(NAME) { + + private var attached by mutableStateOf(false) + + /** + * Get the back stack from the [state]. In some cases, the [sheetContent] might be composed + * before the Navigator is attached, so we specifically return an empty flow if we aren't + * attached yet. + */ + private val backStack: StateFlow> + get() = if (attached) { + state.backStack + } else { + MutableStateFlow(emptyList()) + } + + /** + * Get the transitionsInProgress from the [state]. In some cases, the [sheetContent] might be + * composed before the Navigator is attached, so we specifically return an empty flow if we + * aren't attached yet. + */ + internal actual val transitionsInProgress: StateFlow> + get() = if (attached) { + state.transitionsInProgress + } else { + MutableStateFlow(emptySet()) + } + + /** + * Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState] + */ + public actual val navigatorSheetState: BottomSheetNavigatorSheetState = + BottomSheetNavigatorSheetState(sheetState) + + /** + * A [Composable] function that hosts the current sheet content. This should be set as + * sheetContent of your [ModalBottomSheetLayout]. + */ + internal actual val sheetContent: @Composable ColumnScope.() -> Unit = { + val saveableStateHolder = rememberSaveableStateHolder() + val transitionsInProgressEntries by transitionsInProgress.collectAsState() + + // The latest back stack entry, retained until the sheet is completely hidden + // While the back stack is updated immediately, we might still be hiding the sheet, so + // we keep the entry around until the sheet is hidden + val retainedEntry by produceState( + initialValue = null, + key1 = backStack + ) { + backStack + .transform { backStackEntries -> + // Always hide the sheet when the back stack is updated + // Regardless of whether we're popping or pushing, we always want to hide + // the sheet first before deciding whether to re-show it or keep it hidden + try { + sheetState.hide() + } catch (_: CancellationException) { + // We catch but ignore possible cancellation exceptions as we don't want + // them to bubble up and cancel the whole produceState coroutine + } finally { + emit(backStackEntries.lastOrNull()) + } + } + .collect { + value = it + } + } + if (retainedEntry != null) { + LaunchedEffect(retainedEntry) { + sheetState.show() + } + + //there is no BackHandler for non-android targets yet + //BackHandler { + // state.popWithTransition(popUpTo = retainedEntry!!, saveState = false) + //} + } + + SheetContentHost( + backStackEntry = retainedEntry, + sheetState = sheetState, + saveableStateHolder = saveableStateHolder, + onSheetShown = { + transitionsInProgressEntries.forEach(state::markTransitionComplete) + }, + onSheetDismissed = { backStackEntry -> + // Sheet dismissal can be started through popBackStack in which case we have a + // transition that we'll want to complete + if (transitionsInProgressEntries.contains(backStackEntry)) { + state.markTransitionComplete(backStackEntry) + } + // If there is no transition in progress, the sheet has been dimissed by the + // user (for example by tapping on the scrim or through an accessibility action) + // In this case, we will immediately pop without a transition as the sheet has + // already been hidden + else { + state.pop(popUpTo = backStackEntry, saveState = false) + } + } + ) + } + + override fun onAttach(state: NavigatorState) { + super.onAttach(state) + attached = true + } + + override fun createDestination(): Destination = Destination( + navigator = this, + content = {} + ) + + override fun navigate( + entries: List, + navOptions: NavOptions?, + navigatorExtras: Extras? + ) { + entries.fastForEach { entry -> + state.pushWithTransition(entry) + } + } + + override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { + state.popWithTransition(popUpTo, savedState) + } + + /** + * [NavDestination] specific to [BottomSheetNavigator] + */ + public actual class Destination actual constructor( + navigator: BottomSheetNavigator, + internal actual val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit + ) : NavDestination(navigator), FloatingWindow + + private companion object { + private const val NAME = "bottomSheet" + } +} diff --git a/gradle.properties b/gradle.properties index 94330d1fe32c3..0568bd0e7d3e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -106,6 +106,8 @@ artifactRedirecting.androidx.compose.version=1.7.0-beta06 artifactRedirecting.androidx.compose.material3.version=1.3.0-beta05 artifactRedirecting.androidx.compose.foundation.version=1.7.0-beta06 artifactRedirecting.androidx.compose.material.version=1.7.0-beta06 +# The latest version is not published yet: https://mvnrepository.com/artifact/androidx.compose.material/material-navigation +artifactRedirecting.androidx.compose.material.material-navigation.version=1.7.0-beta01 # Look for `COMPOSE_MATERIAL3_COMMON` in libraryversions.toml artifactRedirecting.androidx.compose.material3.common.version=1.0.0-alpha01 # Look for `COMPOSE_MATERIAL3_ADAPTIVE` in libraryversions.toml diff --git a/mpp/build.gradle.kts b/mpp/build.gradle.kts index 3a9083ae31f95..b85db42a5b8d6 100644 --- a/mpp/build.gradle.kts +++ b/mpp/build.gradle.kts @@ -53,6 +53,7 @@ val libraryToComponents = mapOf( ComposeComponent(":compose:material3:material3-common"), ComposeComponent(":compose:material:material-icons-core"), ComposeComponent(":compose:material:material-ripple"), + ComposeComponent(":compose:material:material-navigation"), ComposeComponent(":compose:material3:material3-window-size-class"), ComposeComponent(":compose:runtime:runtime", supportedPlatforms = ComposePlatforms.ALL), ComposeComponent(":compose:runtime:runtime-saveable", supportedPlatforms = ComposePlatforms.ALL), diff --git a/settings.gradle b/settings.gradle index 535a0e03a4dd9..05bbe32fd7e2a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -168,6 +168,7 @@ includeProject(":compose:material:material-lint") includeProject(":compose:material:material-icons-core") includeProject(":compose:material:material-icons-core:material-icons-core-samples", "compose/material/material-icons-core/samples") includeProject(":compose:material:material-icons-extended") +includeProject(":compose:material:material-navigation") includeProject(":compose:material:material-ripple") includeProject(":compose:material:material:icons:generator") // on an empty project it causes an error during sync in IDEA