Skip to content

Commit

Permalink
[GH] [Navigation] Introduce NavBackStackEntry#provideToCompositionLocals
Browse files Browse the repository at this point in the history
…that provides the NavBackStackEntry to the composition locals needed for all Compose-related navigators.

Relnote: Introduced a new NavBackStackEntry#provideToCompositionLocals that provides the NavBackStackEntry to the relevant composition locals.

## Testing

Test: NavBackStackEntryProviderTest

`NavBackStackEntryProviderTest#testSaveableValueInContentIsSaved` is pretty much just copied from [RememberSaveableTest](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt) and as always, I'll be very happy about suggestions on improving the tests and additional tests!

## Issues Fixed

Fixes: b/187229439

This is an imported pull request from #175.

Resolves #175
Github-Pr-Head-Sha: a8100a0
GitOrigin-RevId: 7441a30
Change-Id: I25a46dfefc52adc1411536359a285bfbc4fb2e38
  • Loading branch information
jossiwolf authored and copybara-github committed May 6, 2021
1 parent 6423af5 commit a8c1f68
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 41 deletions.
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)
}
}

0 comments on commit a8c1f68

Please sign in to comment.