Skip to content

Commit 7441a30

Browse files
authored
Merge a8100a0 into 6423af5
2 parents 6423af5 + a8100a0 commit 7441a30

6 files changed

Lines changed: 256 additions & 41 deletions

File tree

navigation/navigation-compose/api/current.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ package androidx.navigation.compose {
2323
method @androidx.navigation.NavDestinationDsl public static androidx.navigation.compose.NamedNavArgument navArgument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> builder);
2424
}
2525

26+
public final class NavBackStackEntryProviderKt {
27+
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);
28+
}
29+
2630
public final class NavGraphBuilderKt {
2731
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);
2832
method public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);

navigation/navigation-compose/api/public_plus_experimental_current.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ package androidx.navigation.compose {
2323
method @androidx.navigation.NavDestinationDsl public static androidx.navigation.compose.NamedNavArgument navArgument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> builder);
2424
}
2525

26+
public final class NavBackStackEntryProviderKt {
27+
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);
28+
}
29+
2630
public final class NavGraphBuilderKt {
2731
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);
2832
method public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);

navigation/navigation-compose/api/restricted_current.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ package androidx.navigation.compose {
2323
method @androidx.navigation.NavDestinationDsl public static androidx.navigation.compose.NamedNavArgument navArgument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> builder);
2424
}
2525

26+
public final class NavBackStackEntryProviderKt {
27+
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);
28+
}
29+
2630
public final class NavGraphBuilderKt {
2731
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);
2832
method public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2021 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.navigation.compose
18+
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.saveable.rememberSaveable
21+
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
22+
import androidx.compose.ui.platform.LocalLifecycleOwner
23+
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
24+
import androidx.compose.ui.test.junit4.StateRestorationTester
25+
import androidx.compose.ui.test.junit4.createComposeRule
26+
import androidx.lifecycle.LifecycleOwner
27+
import androidx.lifecycle.ViewModelStoreOwner
28+
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
29+
import androidx.navigation.NavBackStackEntry
30+
import androidx.navigation.testing.TestNavigatorState
31+
import androidx.savedstate.SavedStateRegistryOwner
32+
import androidx.test.ext.junit.runners.AndroidJUnit4
33+
import androidx.test.filters.LargeTest
34+
import androidx.testutils.TestNavigator
35+
import com.google.common.truth.Truth.assertThat
36+
import com.google.common.truth.Truth.assertWithMessage
37+
import org.junit.Rule
38+
import org.junit.Test
39+
import org.junit.runner.RunWith
40+
41+
@LargeTest
42+
@RunWith(AndroidJUnit4::class)
43+
class NavBackStackEntryProviderTest {
44+
45+
@get:Rule
46+
val composeTestRule = createComposeRule()
47+
48+
@Test
49+
fun testViewModelStoreOwnerProvided() {
50+
val backStackEntry = createBackStackEntry()
51+
var viewModelStoreOwner: ViewModelStoreOwner? = null
52+
53+
composeTestRule.setContent {
54+
val saveableStateHolder = rememberSaveableStateHolder()
55+
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
56+
viewModelStoreOwner = LocalViewModelStoreOwner.current
57+
}
58+
}
59+
60+
assertWithMessage("ViewModelStoreOwner is provided by $backStackEntry")
61+
.that(viewModelStoreOwner).isEqualTo(backStackEntry)
62+
}
63+
64+
@Test
65+
fun testLifecycleOwnerProvided() {
66+
val backStackEntry = createBackStackEntry()
67+
var lifecycleOwner: LifecycleOwner? = null
68+
69+
composeTestRule.setContent {
70+
val saveableStateHolder = rememberSaveableStateHolder()
71+
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
72+
lifecycleOwner = LocalLifecycleOwner.current
73+
}
74+
}
75+
76+
assertWithMessage("LifecycleOwner is provided by $backStackEntry")
77+
.that(lifecycleOwner).isEqualTo(backStackEntry)
78+
}
79+
80+
@Test
81+
fun testLocalSavedStateRegistryOwnerProvided() {
82+
val backStackEntry = createBackStackEntry()
83+
var localSavedStateRegistryOwner: SavedStateRegistryOwner? = null
84+
85+
composeTestRule.setContent {
86+
val saveableStateHolder = rememberSaveableStateHolder()
87+
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
88+
localSavedStateRegistryOwner = LocalSavedStateRegistryOwner.current
89+
}
90+
}
91+
92+
assertWithMessage("LocalSavedStateRegistryOwner is provided by $backStackEntry")
93+
.that(localSavedStateRegistryOwner).isEqualTo(backStackEntry)
94+
}
95+
96+
@Test
97+
fun testSaveableValueInContentIsSaved() {
98+
val backStackEntry = createBackStackEntry()
99+
val restorationTester = StateRestorationTester(composeTestRule)
100+
var array: IntArray? = null
101+
102+
restorationTester.setContent {
103+
val saveableStateHolder = rememberSaveableStateHolder()
104+
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
105+
array = rememberSaveable {
106+
intArrayOf(0)
107+
}
108+
}
109+
}
110+
111+
assertThat(array).isEqualTo(intArrayOf(0))
112+
113+
composeTestRule.runOnUiThread {
114+
array!![0] = 1
115+
// we null it to ensure recomposition happened
116+
array = null
117+
}
118+
119+
restorationTester.emulateSavedInstanceStateRestore()
120+
121+
assertThat(array).isEqualTo(intArrayOf(1))
122+
}
123+
124+
@Test
125+
fun testNonSaveableValueInContentIsNotSaved() {
126+
val backStackEntry = createBackStackEntry()
127+
val restorationTester = StateRestorationTester(composeTestRule)
128+
var nonSaveable: IntArray? = null
129+
val initialValue = intArrayOf(10)
130+
131+
restorationTester.setContent {
132+
val saveableStateHolder = rememberSaveableStateHolder()
133+
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
134+
nonSaveable = remember { initialValue }
135+
}
136+
}
137+
138+
assertThat(nonSaveable).isEqualTo(initialValue)
139+
140+
composeTestRule.runOnUiThread {
141+
nonSaveable!![0] = 1
142+
// we null it to ensure recomposition happened
143+
nonSaveable = null
144+
}
145+
146+
restorationTester.emulateSavedInstanceStateRestore()
147+
148+
assertThat(nonSaveable).isEqualTo(initialValue)
149+
}
150+
151+
private fun createBackStackEntry(): NavBackStackEntry {
152+
val testNavigator = TestNavigator()
153+
val testNavigatorState = TestNavigatorState()
154+
testNavigator.onAttach(testNavigatorState)
155+
val backStackEntry = testNavigatorState.createBackStackEntry(
156+
testNavigator.createDestination(),
157+
null
158+
)
159+
// We navigate to move the NavBackStackEntry to the correct state
160+
testNavigator.navigate(listOf(backStackEntry), null, null)
161+
return backStackEntry
162+
}
163+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2021 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.navigation.compose
18+
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.CompositionLocalProvider
21+
import androidx.compose.runtime.saveable.SaveableStateHolder
22+
import androidx.compose.ui.platform.LocalLifecycleOwner
23+
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
24+
import androidx.lifecycle.SavedStateHandle
25+
import androidx.lifecycle.ViewModel
26+
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
27+
import androidx.lifecycle.viewmodel.compose.viewModel
28+
import androidx.navigation.NavBackStackEntry
29+
import java.util.UUID
30+
31+
/**
32+
* Provides [this] [NavBackStackEntry] as [LocalViewModelStoreOwner], [LocalLifecycleOwner] and
33+
* [LocalSavedStateRegistryOwner] to the [content] and saves the [content]'s saveable states with
34+
* the given [saveableStateHolder].
35+
*
36+
* @param saveableStateHolder The [SaveableStateHolder] that holds the saved states. The same
37+
* holder should be used for all [NavBackStackEntry]s in the encapsulating [Composable] and the
38+
* holder should be hoisted.
39+
* @param content The content [Composable]
40+
*/
41+
@Composable
42+
public fun NavBackStackEntry.LocalOwnersProvider(
43+
saveableStateHolder: SaveableStateHolder,
44+
content: @Composable () -> Unit
45+
) {
46+
CompositionLocalProvider(
47+
LocalViewModelStoreOwner provides this,
48+
LocalLifecycleOwner provides this,
49+
LocalSavedStateRegistryOwner provides this
50+
) {
51+
saveableStateHolder.SaveableStateProvider(content)
52+
}
53+
}
54+
55+
@Composable
56+
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
57+
val viewModel = viewModel<BackStackEntryIdViewModel>()
58+
viewModel.saveableStateHolder = this
59+
SaveableStateProvider(viewModel.id, content)
60+
}
61+
62+
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
63+
64+
private val IdKey = "SaveableStateHolder_BackStackEntryKey"
65+
66+
// we create our own id for each back stack entry to support multiple entries of the same
67+
// destination. this id will be restored by SavedStateHandle
68+
val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also { handle.set(IdKey, it) }
69+
70+
var saveableStateHolder: SaveableStateHolder? = null
71+
72+
// onCleared will be called on the entries removed from the back stack. here we notify
73+
// RestorableStateHolder that we shouldn't save the state for this id, so when we open this
74+
// destination again the state will not be restored.
75+
override fun onCleared() {
76+
super.onCleared()
77+
saveableStateHolder?.removeState(id)
78+
}
79+
}

navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,21 @@ package androidx.navigation.compose
1919
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
2020
import androidx.compose.foundation.layout.Box
2121
import androidx.compose.runtime.Composable
22-
import androidx.compose.runtime.CompositionLocalProvider
2322
import androidx.compose.runtime.DisposableEffect
2423
import androidx.compose.runtime.collectAsState
2524
import androidx.compose.runtime.getValue
2625
import androidx.compose.runtime.remember
27-
import androidx.compose.runtime.saveable.SaveableStateHolder
2826
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
2927
import androidx.compose.ui.Modifier
3028
import androidx.compose.ui.platform.LocalLifecycleOwner
31-
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
3229
import androidx.lifecycle.Lifecycle
33-
import androidx.lifecycle.SavedStateHandle
34-
import androidx.lifecycle.ViewModel
3530
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
36-
import androidx.lifecycle.viewmodel.compose.viewModel
3731
import androidx.navigation.NavDestination
3832
import androidx.navigation.NavGraph
3933
import androidx.navigation.NavGraphBuilder
4034
import androidx.navigation.NavHostController
4135
import androidx.navigation.Navigator
4236
import androidx.navigation.get
43-
import java.util.UUID
4437

4538
/**
4639
* Provides in place in the Compose hierarchy for self contained navigation to occur.
@@ -136,41 +129,9 @@ public fun NavHost(
136129
// while in the scope of the composable, we provide the navBackStackEntry as the
137130
// ViewModelStoreOwner and LifecycleOwner
138131
Box(modifier, propagateMinConstraints = true) {
139-
CompositionLocalProvider(
140-
LocalViewModelStoreOwner provides backStackEntry,
141-
LocalLifecycleOwner provides backStackEntry,
142-
LocalSavedStateRegistryOwner provides backStackEntry
143-
) {
144-
saveableStateHolder.SaveableStateProvider {
145-
destination.content(backStackEntry)
146-
}
132+
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
133+
destination.content(backStackEntry)
147134
}
148135
}
149136
}
150137
}
151-
152-
@Composable
153-
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
154-
val viewModel = viewModel<BackStackEntryIdViewModel>()
155-
viewModel.saveableStateHolder = this
156-
SaveableStateProvider(viewModel.id, content)
157-
}
158-
159-
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
160-
161-
private val IdKey = "SaveableStateHolder_BackStackEntryKey"
162-
163-
// we create our own id for each back stack entry to support multiple entries of the same
164-
// destination. this id will be restored by SavedStateHandle
165-
val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also { handle.set(IdKey, it) }
166-
167-
var saveableStateHolder: SaveableStateHolder? = null
168-
169-
// onCleared will be called on the entries removed from the back stack. here we notify
170-
// RestorableStateHolder that we shouldn't save the state for this id, so when we open this
171-
// destination again the state will not be restored.
172-
override fun onCleared() {
173-
super.onCleared()
174-
saveableStateHolder?.removeState(id)
175-
}
176-
}

0 commit comments

Comments
 (0)