Upstream Navigation (#744)

* Official Navigation

* Update snapshot

* Move navigation to snapshot

* Drop dead switch

* Bisect

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2024-02-17 12:16:05 +08:00
1 parent 3cb1b1eee4
commit c4045ded70
15 files changed
+3 -1258

No files matched your search

+1 -8
View File
@@ -210,10 +210,9 @@ dependencies {
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
// https://developer.android.com/jetpack/androidx/releases/navigation
implementation(libs.bundles.androidx.navigation)
implementation(libs.androidx.navigation.compose)
// https://developer.android.com/jetpack/androidx/releases/paging
implementation(libs.androidx.paging.compose)
@@ -269,12 +268,6 @@ dependencies {
"gmsImplementation"(libs.bundles.cronet)
}
configurations.all {
resolutionStrategy {
exclude("androidx.navigation", "navigation-compose")
}
}
kotlin {
jvmToolchain(21)
}
@@ -1,63 +0,0 @@
/*
* Copyright 2023 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
*
* http://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.navigation.compose
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphNavigator
import androidx.navigation.Navigator
import androidx.navigation.NavigatorProvider
/**
* Custom subclass of [NavGraphNavigator] that adds support for defining
* transitions at the navigation graph level.
*/
@Navigator.Name("navigation")
internal class ComposeNavGraphNavigator(
navigatorProvider: NavigatorProvider,
) : NavGraphNavigator(navigatorProvider) {
override fun createDestination(): NavGraph {
return ComposeNavGraph(this)
}
internal class ComposeNavGraph(
navGraphNavigator: Navigator<out NavGraph>,
) : NavGraph(navGraphNavigator) {
internal var enterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? = null
internal var exitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? = null
internal var popEnterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? = null
internal var popExitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? = null
}
}
@@ -1,128 +0,0 @@
/*
* Copyright 2020 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
*
* http://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.navigation.compose
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.compose.ComposeNavigator.Destination
/**
* Navigator that navigates through [Composable]s. Every destination using this Navigator must
* set a valid [Composable] by setting it directly on an instantiated [Destination] or calling
* [composable].
*/
@Navigator.Name("composable")
public class ComposeNavigator : Navigator<Destination>() {
/**
* Get the map of transitions currently in progress from the [state].
*/
internal val transitionsInProgress get() = state.transitionsInProgress
/**
* Get the back stack from the [state].
*/
public val backStack get() = state.backStack
internal val isPop = mutableStateOf(false)
override fun navigate(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Extras?,
) {
entries.forEach { entry ->
state.pushWithTransition(entry)
}
isPop.value = false
}
override fun createDestination(): Destination {
return Destination(this) { }
}
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
state.popWithTransition(popUpTo, savedState)
isPop.value = true
}
/**
* Callback to mark a navigation in transition as complete.
*
* This should be called in conjunction with [navigate] and [popBackStack] as those
* calls merely start a transition to the target destination, and requires manually marking
* the transition as complete by calling this method.
*
* Failing to call this method could result in entries being prevented from reaching their
* final [Lifecycle.State].
*/
public fun onTransitionComplete(entry: NavBackStackEntry) {
state.markTransitionComplete(entry)
}
/**
* NavDestination specific to [ComposeNavigator]
*/
@NavDestination.ClassType(Composable::class)
public class Destination(
navigator: ComposeNavigator,
internal val content:
@Composable AnimatedContentScope.(@JvmSuppressWildcards NavBackStackEntry) -> Unit,
) : NavDestination(navigator) {
@Deprecated(
message = "Deprecated in favor of Destination that supports AnimatedContent",
level = DeprecationLevel.HIDDEN,
)
constructor(
navigator: ComposeNavigator,
content: @Composable (NavBackStackEntry) -> @JvmSuppressWildcards Unit,
) : this(navigator, content = { entry -> content(entry) })
internal var enterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? = null
internal var exitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? = null
internal var popEnterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? = null
internal var popExitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? = null
}
internal companion object {
internal const val NAME = "composable"
}
}
@@ -1,137 +0,0 @@
/*
* Copyright 2021 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
*
* http://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.navigation.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.DialogNavigator.Destination
/**
* Show each [Destination] on the [DialogNavigator]'s back stack as a [Dialog].
*
* Note that [NavHost] will call this for you; you do not need to call it manually.
*/
@Composable
public fun DialogHost(dialogNavigator: DialogNavigator) {
val saveableStateHolder = rememberSaveableStateHolder()
val dialogBackStack by dialogNavigator.backStack.collectAsState()
val visibleBackStack = rememberVisibleList(dialogBackStack)
visibleBackStack.PopulateVisibleList(dialogBackStack)
val transitionInProgress by dialogNavigator.transitionInProgress.collectAsState()
val dialogsToDispose = remember { mutableStateListOf<NavBackStackEntry>() }
visibleBackStack.forEach { backStackEntry ->
val destination = backStackEntry.destination as Destination
Dialog(
onDismissRequest = { dialogNavigator.dismiss(backStackEntry) },
properties = destination.dialogProperties,
) {
DisposableEffect(backStackEntry) {
dialogsToDispose.add(backStackEntry)
onDispose {
dialogNavigator.onTransitionComplete(backStackEntry)
dialogsToDispose.remove(backStackEntry)
}
}
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
destination.content(backStackEntry)
}
}
}
// Dialogs may have been popped before it was composed. To prevent leakage, we need to
// mark popped entries as complete here. Check that we don't accidentally complete popped
// entries that were composed, unless they were disposed of already.
LaunchedEffect(transitionInProgress, dialogsToDispose) {
transitionInProgress.forEach { entry ->
if (!dialogNavigator.backStack.value.contains(entry) &&
!dialogsToDispose.contains(entry)
) {
dialogNavigator.onTransitionComplete(entry)
}
}
}
}
@Composable
internal fun MutableList<NavBackStackEntry>.PopulateVisibleList(
backStack: Collection<NavBackStackEntry>,
) {
val isInspecting = LocalInspectionMode.current
backStack.forEach { entry ->
DisposableEffect(entry.lifecycle) {
val observer = LifecycleEventObserver { _, event ->
// show dialog in preview
if (isInspecting && !contains(entry)) {
add(entry)
}
// ON_START -> add to visibleBackStack, ON_STOP -> remove from visibleBackStack
if (event == Lifecycle.Event.ON_START) {
// We want to treat the visible lists as Sets but we want to keep
// the functionality of mutableStateListOf() so that we recompose in response
// to adds and removes.
if (!contains(entry)) {
add(entry)
}
}
if (event == Lifecycle.Event.ON_STOP) {
remove(entry)
}
}
entry.lifecycle.addObserver(observer)
onDispose {
entry.lifecycle.removeObserver(observer)
}
}
}
}
@Composable
internal fun rememberVisibleList(
backStack: Collection<NavBackStackEntry>,
): SnapshotStateList<NavBackStackEntry> {
// show dialog in preview
val isInspecting = LocalInspectionMode.current
return remember(backStack) {
mutableStateListOf<NavBackStackEntry>().also {
it.addAll(
backStack.filter { entry ->
if (isInspecting) {
true
} else {
entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
},
)
}
}
}
@@ -1,96 +0,0 @@
/*
* Copyright 2020 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
*
* http://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.navigation.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.FloatingWindow
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.compose.DialogNavigator.Destination
/**
* Navigator that navigates through [Composable]s that will be hosted within a
* [Dialog]. Every destination using this Navigator must set a valid [Composable] by setting it
* directly on an instantiated [Destination] or calling [dialog].
*/
@Navigator.Name("dialog")
public class DialogNavigator : Navigator<Destination>() {
/**
* Get the back stack from the [state].
*/
internal val backStack get() = state.backStack
/**
* Get the transitioning dialogs from the [state].
*/
internal val transitionInProgress get() = state.transitionsInProgress
/**
* Dismiss the dialog destination associated with the given [backStackEntry].
*/
internal fun dismiss(backStackEntry: NavBackStackEntry) {
popBackStack(backStackEntry, false)
}
override fun navigate(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Extras?,
) {
entries.forEach { entry ->
state.push(entry)
}
}
override fun createDestination(): Destination {
return Destination(this) { }
}
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
state.popWithTransition(popUpTo, savedState)
// When popping, the incoming dialog is marked transitioning to hold it in
// STARTED. With pop complete, we can remove it from transition so it can move to RESUMED.
val popIndex = state.transitionsInProgress.value.indexOf(popUpTo)
// do not mark complete for entries up to and including popUpTo
state.transitionsInProgress.value.forEachIndexed { index, entry ->
if (index > popIndex) onTransitionComplete(entry)
}
}
internal fun onTransitionComplete(entry: NavBackStackEntry) {
state.markTransitionComplete(entry)
}
/**
* NavDestination specific to [DialogNavigator]
*/
@NavDestination.ClassType(Composable::class)
public class Destination(
navigator: DialogNavigator,
internal val dialogProperties: DialogProperties = DialogProperties(),
internal val content: @Composable (NavBackStackEntry) -> Unit,
) : NavDestination(navigator), FloatingWindow
internal companion object {
internal const val NAME = "dialog"
}
}
@@ -1,88 +0,0 @@
/*
* Copyright 2021 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
*
* http://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.
*/
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
package androidx.navigation.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import java.lang.ref.WeakReference
import java.util.UUID
/**
* Provides [this] [NavBackStackEntry] as [LocalViewModelStoreOwner], [LocalLifecycleOwner] and
* [LocalSavedStateRegistryOwner] to the [content] and saves the [content]'s saveable states with
* the given [saveableStateHolder].
*
* @param saveableStateHolder The [SaveableStateHolder] that holds the saved states. The same
* holder should be used for all [NavBackStackEntry]s in the encapsulating [Composable] and the
* holder should be hoisted.
* @param content The content [Composable]
*/
@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
saveableStateHolder: SaveableStateHolder,
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalViewModelStoreOwner provides this,
LocalLifecycleOwner provides this,
LocalSavedStateRegistryOwner provides this,
) {
saveableStateHolder.SaveableStateProvider(content)
}
}
@Composable
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
val viewModel = viewModel<BackStackEntryIdViewModel>()
// Stash a reference to the SaveableStateHolder in the ViewModel so that
// it is available when the ViewModel is cleared, marking the permanent removal of this
// NavBackStackEntry from the back stack. Which, because of animations,
// only happens after this leaves composition. Which means we can't rely on
// DisposableEffect to clean up this reference (as it'll be cleaned up too early)
viewModel.saveableStateHolderRef = WeakReference(this)
SaveableStateProvider(viewModel.id, content)
}
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
private val IdKey = "SaveableStateHolder_BackStackEntryKey"
// we create our own id for each back stack entry to support multiple entries of the same
// destination. this id will be restored by SavedStateHandle
val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also { handle.set(IdKey, it) }
lateinit var saveableStateHolderRef: WeakReference<SaveableStateHolder>
// onCleared will be called on the entries removed from the back stack. here we notify
// SaveableStateProvider that we should remove any state is had associated with this
// destination as it is no longer needed.
override fun onCleared() {
super.onCleared()
saveableStateHolderRef.get()?.removeState(id)
saveableStateHolderRef.clear()
}
}
@@ -1,243 +0,0 @@
/*
* Copyright 2020 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
*
* http://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.navigation.compose
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphBuilder
import androidx.navigation.get
/**
* Add the [Composable] to the [NavGraphBuilder]
*
* @param route route for the destination
* @param arguments list of arguments to associate with destination
* @param deepLinks list of deep links to associate with the destinations
* @param content composable for the destination
*/
@Deprecated(
message = "Deprecated in favor of composable builder that supports AnimatedContent",
level = DeprecationLevel.HIDDEN,
)
public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit,
) {
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class]) {
entry ->
content(entry)
}.apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
},
)
}
/**
* Add the [Composable] to the [NavGraphBuilder]
*
* @param route route for the destination
* @param arguments list of arguments to associate with destination
* @param deepLinks list of deep links to associate with the destinations
* @param enterTransition callback to determine the destination's enter transition
* @param exitTransition callback to determine the destination's exit transition
* @param popEnterTransition callback to determine the destination's popEnter transition
* @param popExitTransition callback to determine the destination's popExit transition
* @param content composable for the destination
*/
public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
enterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? = null,
exitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? = null,
popEnterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? =
enterTransition,
popExitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? =
exitTransition,
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
addDestination(
ComposeNavigator.Destination(
provider[ComposeNavigator::class],
content,
).apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
this.enterTransition = enterTransition
this.exitTransition = exitTransition
this.popEnterTransition = popEnterTransition
this.popExitTransition = popExitTransition
},
)
}
/**
* Construct a nested [NavGraph]
*
* @sample androidx.navigation.compose.samples.NestedNavInGraphWithArgs
*
* @param startDestination the starting destination's route for this NavGraph
* @param route the destination's unique route
* @param arguments list of arguments to associate with destination
* @param deepLinks list of deep links to associate with the destinations
* @param builder the builder used to construct the graph
*/
@Deprecated(
message = "Deprecated in favor of navigation builder that supports AnimatedContent",
level = DeprecationLevel.HIDDEN,
)
public fun NavGraphBuilder.navigation(
startDestination: String,
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
builder: NavGraphBuilder.() -> Unit,
) {
addDestination(
NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply {
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
},
)
}
/**
* Construct a nested [NavGraph]
*
* @param startDestination the starting destination's route for this NavGraph
* @param route the destination's unique route
* @param arguments list of arguments to associate with destination
* @param deepLinks list of deep links to associate with the destinations
* @param enterTransition callback to define enter transitions for destination in this NavGraph
* @param exitTransition callback to define exit transitions for destination in this NavGraph
* @param popEnterTransition callback to define pop enter transitions for destination in this
* NavGraph
* @param popExitTransition callback to define pop exit transitions for destination in this NavGraph
* @param builder the builder used to construct the graph
*
* @return the newly constructed nested NavGraph
*/
public fun NavGraphBuilder.navigation(
startDestination: String,
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
null,
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? =
null,
popEnterTransition: (
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? = enterTransition,
popExitTransition: (
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? = exitTransition,
builder: NavGraphBuilder.() -> Unit,
) {
addDestination(
NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply {
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
if (this is ComposeNavGraphNavigator.ComposeNavGraph) {
this.enterTransition = enterTransition
this.exitTransition = exitTransition
this.popEnterTransition = popEnterTransition
this.popExitTransition = popExitTransition
}
},
)
}
/**
* Add the [Composable] to the [NavGraphBuilder] that will be hosted within a
* [androidx.compose.ui.window.Dialog]. This is suitable only when this dialog represents
* a separate screen in your app that needs its own lifecycle and saved state, independent
* of any other destination in your navigation graph. For use cases such as `AlertDialog`,
* you should use those APIs directly in the [composable] destination that wants to show that
* dialog.
*
* @param route route for the destination
* @param arguments list of arguments to associate with destination
* @param deepLinks list of deep links to associate with the destinations
* @param dialogProperties properties that should be passed to [androidx.compose.ui.window.Dialog].
* @param content composable content for the destination that will be hosted within the Dialog
*/
public fun NavGraphBuilder.dialog(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
dialogProperties: DialogProperties = DialogProperties(),
content: @Composable (NavBackStackEntry) -> Unit,
) {
addDestination(
DialogNavigator.Destination(
provider[DialogNavigator::class],
dialogProperties,
content,
).apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
},
)
}
@@ -1,397 +0,0 @@
/*
* Copyright 2020 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
*
* http://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.navigation.compose
import android.annotation.SuppressLint
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.ExperimentalTransitionApi
import androidx.compose.animation.core.SeekableTransitionState
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.Navigator
import androidx.navigation.createGraph
import androidx.navigation.get
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.collectAsState
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
/**
* Provides in place in the Compose hierarchy for self contained navigation to occur.
*
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
* the provided [navController].
*
* The builder passed into this method is [remember]ed. This means that for this NavHost, the
* contents of the builder cannot be changed.
*
* @sample androidx.navigation.compose.samples.NavScaffold
*
* @param navController the navController for this host
* @param startDestination the route for the start destination
* @param modifier The modifier to be applied to the layout.
* @param route the route for the graph
* @param builder the builder used to construct the graph
*/
@Deprecated(
message = "Deprecated in favor of NavHost that supports AnimatedContent",
level = DeprecationLevel.HIDDEN,
)
@Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit,
) {
NavHost(
navController,
remember(route, startDestination, builder) {
navController.createGraph(startDestination, route, builder)
},
modifier,
)
}
/**
* Provides in place in the Compose hierarchy for self contained navigation to occur.
*
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
* the provided [navController].
*
* The builder passed into this method is [remember]ed. This means that for this NavHost, the
* contents of the builder cannot be changed.
*
* @param navController the navController for this host
* @param startDestination the route for the start destination
* @param modifier The modifier to be applied to the layout.
* @param contentAlignment The [Alignment] of the [AnimatedContent]
* @param route the route for the graph
* @param enterTransition callback to define enter transitions for destination in this host
* @param exitTransition callback to define exit transitions for destination in this host
* @param popEnterTransition callback to define popEnter transitions for destination in this host
* @param popExitTransition callback to define popExit transitions for destination in this host
* @param builder the builder used to construct the graph
*/
@Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.Center,
route: String? = null,
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
{ fadeIn(animationSpec = tween(700)) },
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
{ fadeOut(animationSpec = tween(700)) },
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
enterTransition,
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
exitTransition,
builder: NavGraphBuilder.() -> Unit,
) {
NavHost(
navController,
remember(route, startDestination, builder) {
navController.createGraph(startDestination, route, builder)
},
modifier,
contentAlignment,
enterTransition,
exitTransition,
popEnterTransition,
popExitTransition,
)
}
/**
* Provides in place in the Compose hierarchy for self contained navigation to occur.
*
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
* the provided [navController].
*
* The graph passed into this method is [remember]ed. This means that for this NavHost, the graph
* cannot be changed.
*
* @param navController the navController for this host
* @param graph the graph for this host
* @param modifier The modifier to be applied to the layout.
*/
@Deprecated(
message = "Deprecated in favor of NavHost that supports AnimatedContent",
level = DeprecationLevel.HIDDEN,
)
@Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier,
) = NavHost(navController, graph, modifier)
/**
* Provides in place in the Compose hierarchy for self contained navigation to occur.
*
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
* the provided [navController].
*
* @param navController the navController for this host
* @param graph the graph for this host
* @param modifier The modifier to be applied to the layout.
* @param contentAlignment The [Alignment] of the [AnimatedContent]
* @param enterTransition callback to define enter transitions for destination in this host
* @param exitTransition callback to define exit transitions for destination in this host
* @param popEnterTransition callback to define popEnter transitions for destination in this host
* @param popExitTransition callback to define popExit transitions for destination in this host
*/
@OptIn(ExperimentalTransitionApi::class)
@SuppressLint("StateFlowValueCalledInComposition")
@Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.Center,
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
{ fadeIn(animationSpec = tween(700)) },
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
{ fadeOut(animationSpec = tween(700)) },
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
enterTransition,
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
exitTransition,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
}
navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
// Then set the graph
navController.graph = graph
// Find the ComposeNavigator, returning early if it isn't found
// (such as is the case when using TestNavHostController)
val composeNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
ComposeNavigator.NAME,
) as? ComposeNavigator ?: return
val currentBackStack by composeNavigator.backStack.collectAsState()
DisposableEffect(lifecycleOwner) {
// Setup the navController with proper owners
navController.setLifecycleOwner(lifecycleOwner)
onDispose { }
}
val saveableStateHolder = rememberSaveableStateHolder()
val allVisibleEntries by navController.visibleEntries.collectAsState()
// Intercept back only when there's a destination to pop
val visibleEntries by remember {
derivedStateOf {
allVisibleEntries.filter { entry ->
entry.destination.navigatorName == ComposeNavigator.NAME
}
}
}
val backStackEntry: NavBackStackEntry? = visibleEntries.lastOrNull()
val zIndices = remember { mutableMapOf<String, Float>() }
if (backStackEntry != null) {
val enablePredictiveBack by Settings.predictiveNavAnim.collectAsState()
val transitionState = remember { SeekableTransitionState(backStackEntry) }
val transition = if (enablePredictiveBack) {
LaunchedEffect(backStackEntry) {
transitionState.animateTo(backStackEntry)
}
rememberTransition(transitionState, label = "entry")
} else {
updateTransition(backStackEntry, label = "entry")
}
var inPredictiveBack by remember { mutableStateOf(false) }
PredictiveBackHandler(currentBackStack.size > 1) { backEvent ->
inPredictiveBack = true
try {
val prev = requireNotNull(navController.previousBackStackEntry)
backEvent.collect {
val interpolated = EaseOut.transform(it.progress)
transitionState.snapTo(prev, interpolated)
}
inPredictiveBack = false
navController.popBackStack()
} catch (e: CancellationException) {
withContext(NonCancellable) {
transitionState.animateTo(backStackEntry, snap())
inPredictiveBack = false
}
}
}
val finalEnter: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
val targetDestination = targetState.destination as ComposeNavigator.Destination
if (composeNavigator.isPop.value || inPredictiveBack) {
targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
destination.createPopEnterTransition(this)
} ?: popEnterTransition.invoke(this)
} else {
targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
destination.createEnterTransition(this)
} ?: enterTransition.invoke(this)
}
}
val finalExit: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
val initialDestination = initialState.destination as ComposeNavigator.Destination
if (composeNavigator.isPop.value || inPredictiveBack) {
initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
destination.createPopExitTransition(this)
} ?: popExitTransition.invoke(this)
} else {
initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
destination.createExitTransition(this)
} ?: exitTransition.invoke(this)
}
}
DisposableEffect(true) {
onDispose {
visibleEntries.forEach { entry ->
composeNavigator.onTransitionComplete(entry)
}
}
}
transition.AnimatedContent(
modifier,
transitionSpec = {
// If the initialState of the AnimatedContent is not in visibleEntries, we are in
// a case where visible has cleared the old state for some reason, so instead of
// attempting to animate away from the initialState, we skip the animation.
if (initialState in visibleEntries) {
val initialZIndex = zIndices[initialState.id]
?: 0f.also { zIndices[initialState.id] = 0f }
val targetZIndex = when {
targetState.id == initialState.id -> initialZIndex
composeNavigator.isPop.value -> initialZIndex - 1f
else -> initialZIndex + 1f
}.also { zIndices[targetState.id] = it }
ContentTransform(finalEnter(this), finalExit(this), targetZIndex)
} else {
EnterTransition.None togetherWith ExitTransition.None
}
},
contentAlignment,
contentKey = { it.id },
) { currentEntry ->
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
currentEntry.LocalOwnersProvider(saveableStateHolder) {
(currentEntry.destination as ComposeNavigator.Destination)
.content(this, currentEntry)
}
}
LaunchedEffect(transition.currentState, transition.targetState) {
if (transition.currentState == transition.targetState) {
visibleEntries.forEach { entry ->
composeNavigator.onTransitionComplete(entry)
}
zIndices
.filter { it.key != transition.targetState.id }
.forEach { zIndices.remove(it.key) }
}
}
}
val dialogNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
DialogNavigator.NAME,
) as? DialogNavigator ?: return
// Show any dialog destinations
DialogHost(dialogNavigator)
}
private fun NavDestination.createEnterTransition(
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
): EnterTransition? = when (this) {
is ComposeNavigator.Destination -> this.enterTransition?.invoke(scope)
is ComposeNavGraphNavigator.ComposeNavGraph -> this.enterTransition?.invoke(scope)
else -> null
}
private fun NavDestination.createExitTransition(
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
): ExitTransition? = when (this) {
is ComposeNavigator.Destination -> this.exitTransition?.invoke(scope)
is ComposeNavGraphNavigator.ComposeNavGraph -> this.exitTransition?.invoke(scope)
else -> null
}
private fun NavDestination.createPopEnterTransition(
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
): EnterTransition? = when (this) {
is ComposeNavigator.Destination -> this.popEnterTransition?.invoke(scope)
is ComposeNavGraphNavigator.ComposeNavGraph -> this.popEnterTransition?.invoke(scope)
else -> null
}
private fun NavDestination.createPopExitTransition(
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
): ExitTransition? = when (this) {
is ComposeNavigator.Destination -> this.popExitTransition?.invoke(scope)
is ComposeNavGraphNavigator.ComposeNavGraph -> this.popExitTransition?.invoke(scope)
else -> null
}
@@ -1,86 +0,0 @@
/*
* Copyright 2020 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
*
* http://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.
*/
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
package androidx.navigation.compose
import android.content.Context
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavHostController
import androidx.navigation.Navigator
/**
* Gets the current navigation back stack entry as a [MutableState]. When the given navController
* changes the back stack due to a [NavController.navigate] or [NavController.popBackStack] this
* will trigger a recompose and return the top entry on the back stack.
*
* @return a mutable state of the current back stack entry
*/
@Composable
public fun NavController.currentBackStackEntryAsState(): State<NavBackStackEntry?> {
return currentBackStackEntryFlow.collectAsState(null)
}
/**
* Creates a NavHostController that handles the adding of the [ComposeNavigator] and
* [DialogNavigator]. Additional [Navigator] instances can be passed through [navigators] to
* be applied to the returned NavController. Note that each [Navigator] must be separately
* remembered before being passed in here: any changes to those inputs will cause the
* NavController to be recreated.
*
* @see NavHost
*/
@Composable
public fun rememberNavController(
vararg navigators: Navigator<out NavDestination>,
): NavHostController {
val context = LocalContext.current
return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) {
createNavController(context)
}.apply {
for (navigator in navigators) {
navigatorProvider.addNavigator(navigator)
}
}
}
private fun createNavController(context: Context) =
NavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavGraphNavigator(navigatorProvider))
navigatorProvider.addNavigator(ComposeNavigator())
navigatorProvider.addNavigator(DialogNavigator())
}
/**
* Saver to save and restore the NavController across config change and process death.
*/
private fun NavControllerSaver(
context: Context,
): Saver<NavHostController, *> = Saver<NavHostController, Bundle>(
save = { it.saveState() },
restore = { createNavController(context).apply { restoreState(it) } },
)
@@ -109,7 +109,6 @@ object Settings : DataStorePreferences(null) {
val listThumbSize = intPref("list_tile_size", 40)
val languageFilter = intPref("language_filter", -1)
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
val predictiveNavAnim = boolPref("predictive_nav_anim_2", false)
val blackDarkTheme = boolPref("black_dark_theme", false)
var downloadScheme by stringOrNullPref("image_scheme", null)
@@ -176,10 +176,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
title = stringResource(id = R.string.preload_thumb_aggressively),
value = Settings::preloadThumbAggressively,
)
SwitchPreference(
title = stringResource(id = R.string.predictive_nav_anim),
value = Settings.predictiveNavAnim::value,
)
IntSliderPreference(
maxValue = 5,
minValue = 1,
@@ -498,7 +498,6 @@
<string name="list_tile_thumb_size">详情模式下缩略图大小</string>
<string name="account_name">账户</string>
<string name="preload_thumb_aggressively">激进地预载缩略图</string>
<string name="predictive_nav_anim">[实验性] 预见式返回导航动画</string>
<string name="auto_updates">自动检查更新</string>
<string name="update_frequency_never">从不</string>
<string name="update_frequency_daily">每天</string>
-1
View File
@@ -561,7 +561,6 @@
<string name="list_tile_thumb_size">Thumb size in detail mode</string>
<string name="account_name">Account</string>
<string name="preload_thumb_aggressively">Preload thumbs aggressively</string>
<string name="predictive_nav_anim">[Experimental] Predictive back navigation animations</string>
<string name="user_agent">User Agent</string>
<string name="auto_updates">Automatically check for updates</string>
<string name="update_frequency_never">Never</string>
+1 -4
View File
@@ -10,7 +10,7 @@ androidx-constraintlayout-compose = "1.1.0-alpha13"
androidx-core = "1.12.0"
androidx-core-splashscreen = "1.1.0-alpha02"
androidx-lifecycle = "2.7.0"
androidx-navigation = "2.7.7"
androidx-navigation = "2.8.0-SNAPSHOT"
androidx-paging = "3.2.1"
androidx-recyclerview = "1.3.2"
androidx-room = "2.6.1"
@@ -45,10 +45,8 @@ androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-cor
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" }
androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
androidx-navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" }
@@ -123,7 +121,6 @@ splitties-preferences = { module = "com.louiscad.splitties:splitties-preferences
splitties-systemservices = { module = "com.louiscad.splitties:splitties-systemservices", version.ref = "splitties" }
[bundles]
androidx-navigation = ["androidx-navigation-compose", "androidx-navigation-ui"]
coil = ["coil-compose", "coil-gif", "coil-network"]
compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-windowsizeclass", "compose-ui-util"]
cronet = ["cronet-api", "cronet-gms"]
+1 -1
View File
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
mavenCentral()
maven("https://jitpack.io")
maven("https://androidx.dev/storage/compose-compiler/repository/")
maven("https://androidx.dev/snapshots/builds/11458419/artifacts/repository")
maven("https://androidx.dev/snapshots/builds/11461852/artifacts/repository")
}
}