这是indexloc提供的服务,不要输入任何密码
Skip to content

[Navigation] Introduce NavBackStackEntry#provideToCompositionLocals #175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4 changes: 4 additions & 0 deletions navigation/navigation-compose/api/current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ package androidx.navigation.compose {
method @androidx.navigation.NavDestinationDsl public static androidx.navigation.compose.NamedNavArgument navArgument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> builder);
}

public final class NavBackStackEntryProviderKt {
method @androidx.compose.runtime.Composable public static void LocalOwnersProvider(androidx.navigation.NavBackStackEntry, androidx.compose.runtime.saveable.SaveableStateHolder saveableStateHolder, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}

public final class NavGraphBuilderKt {
method public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.compose.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ package androidx.navigation.compose {
method @androidx.navigation.NavDestinationDsl public static androidx.navigation.compose.NamedNavArgument navArgument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> builder);
}

public final class NavBackStackEntryProviderKt {
method @androidx.compose.runtime.Composable public static void LocalOwnersProvider(androidx.navigation.NavBackStackEntry, androidx.compose.runtime.saveable.SaveableStateHolder saveableStateHolder, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}

public final class NavGraphBuilderKt {
method public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.compose.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
Expand Down
4 changes: 4 additions & 0 deletions navigation/navigation-compose/api/restricted_current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ package androidx.navigation.compose {
method @androidx.navigation.NavDestinationDsl public static androidx.navigation.compose.NamedNavArgument navArgument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> builder);
}

public final class NavBackStackEntryProviderKt {
method @androidx.compose.runtime.Composable public static void LocalOwnersProvider(androidx.navigation.NavBackStackEntry, androidx.compose.runtime.saveable.SaveableStateHolder saveableStateHolder, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}

public final class NavGraphBuilderKt {
method public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.compose.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.navigation.NavBackStackEntry
import androidx.navigation.testing.TestNavigatorState
import androidx.savedstate.SavedStateRegistryOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.testutils.TestNavigator
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@LargeTest
@RunWith(AndroidJUnit4::class)
class NavBackStackEntryProviderTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testViewModelStoreOwnerProvided() {
val backStackEntry = createBackStackEntry()
var viewModelStoreOwner: ViewModelStoreOwner? = null

composeTestRule.setContent {
val saveableStateHolder = rememberSaveableStateHolder()
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
viewModelStoreOwner = LocalViewModelStoreOwner.current
}
}

assertWithMessage("ViewModelStoreOwner is provided by $backStackEntry")
.that(viewModelStoreOwner).isEqualTo(backStackEntry)
}

@Test
fun testLifecycleOwnerProvided() {
val backStackEntry = createBackStackEntry()
var lifecycleOwner: LifecycleOwner? = null

composeTestRule.setContent {
val saveableStateHolder = rememberSaveableStateHolder()
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
lifecycleOwner = LocalLifecycleOwner.current
}
}

assertWithMessage("LifecycleOwner is provided by $backStackEntry")
.that(lifecycleOwner).isEqualTo(backStackEntry)
}

@Test
fun testLocalSavedStateRegistryOwnerProvided() {
val backStackEntry = createBackStackEntry()
var localSavedStateRegistryOwner: SavedStateRegistryOwner? = null

composeTestRule.setContent {
val saveableStateHolder = rememberSaveableStateHolder()
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
localSavedStateRegistryOwner = LocalSavedStateRegistryOwner.current
}
}

assertWithMessage("LocalSavedStateRegistryOwner is provided by $backStackEntry")
.that(localSavedStateRegistryOwner).isEqualTo(backStackEntry)
}

@Test
fun testSaveableValueInContentIsSaved() {
val backStackEntry = createBackStackEntry()
val restorationTester = StateRestorationTester(composeTestRule)
var array: IntArray? = null

restorationTester.setContent {
val saveableStateHolder = rememberSaveableStateHolder()
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
array = rememberSaveable {
intArrayOf(0)
}
}
}

assertThat(array).isEqualTo(intArrayOf(0))

composeTestRule.runOnUiThread {
array!![0] = 1
// we null it to ensure recomposition happened
array = null
}

restorationTester.emulateSavedInstanceStateRestore()

assertThat(array).isEqualTo(intArrayOf(1))
}

@Test
fun testNonSaveableValueInContentIsNotSaved() {
val backStackEntry = createBackStackEntry()
val restorationTester = StateRestorationTester(composeTestRule)
var nonSaveable: IntArray? = null
val initialValue = intArrayOf(10)

restorationTester.setContent {
val saveableStateHolder = rememberSaveableStateHolder()
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
nonSaveable = remember { initialValue }
}
}

assertThat(nonSaveable).isEqualTo(initialValue)

composeTestRule.runOnUiThread {
nonSaveable!![0] = 1
// we null it to ensure recomposition happened
nonSaveable = null
}

restorationTester.emulateSavedInstanceStateRestore()

assertThat(nonSaveable).isEqualTo(initialValue)
}

private fun createBackStackEntry(): NavBackStackEntry {
val testNavigator = TestNavigator()
val testNavigatorState = TestNavigatorState()
testNavigator.onAttach(testNavigatorState)
val backStackEntry = testNavigatorState.createBackStackEntry(
testNavigator.createDestination(),
null
)
// We navigate to move the NavBackStackEntry to the correct state
testNavigator.navigate(listOf(backStackEntry), null, null)
return backStackEntry
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.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.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>()
viewModel.saveableStateHolder = 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) }

var saveableStateHolder: SaveableStateHolder? = null

// onCleared will be called on the entries removed from the back stack. here we notify
// RestorableStateHolder that we shouldn't save the state for this id, so when we open this
// destination again the state will not be restored.
override fun onCleared() {
super.onCleared()
saveableStateHolder?.removeState(id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,21 @@ package androidx.navigation.compose
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.Navigator
import androidx.navigation.get
import java.util.UUID

/**
* Provides in place in the Compose hierarchy for self contained navigation to occur.
Expand Down Expand Up @@ -136,41 +129,9 @@ public fun NavHost(
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
Box(modifier, propagateMinConstraints = true) {
CompositionLocalProvider(
LocalViewModelStoreOwner provides backStackEntry,
LocalLifecycleOwner provides backStackEntry,
LocalSavedStateRegistryOwner provides backStackEntry
) {
saveableStateHolder.SaveableStateProvider {
destination.content(backStackEntry)
}
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
destination.content(backStackEntry)
}
}
}
}

@Composable
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
val viewModel = viewModel<BackStackEntryIdViewModel>()
viewModel.saveableStateHolder = 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) }

var saveableStateHolder: SaveableStateHolder? = null

// onCleared will be called on the entries removed from the back stack. here we notify
// RestorableStateHolder that we shouldn't save the state for this id, so when we open this
// destination again the state will not be restored.
override fun onCleared() {
super.onCleared()
saveableStateHolder?.removeState(id)
}
}