Merge changes from topic "snapshots_on" into androidx-main
* changes:
Turn on snapshots for nav3 libraries
Add new ViewModelStoreNavContentWrapper
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/androidx-lifecycle-viewmodel-navigation3-documentation.md b/lifecycle/lifecycle-viewmodel-navigation3/androidx-lifecycle-viewmodel-navigation3-documentation.md
new file mode 100644
index 0000000..d5cea18
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-navigation3/androidx-lifecycle-viewmodel-navigation3-documentation.md
@@ -0,0 +1,14 @@
+# EXPERIMENTAL
+
+## This is an **EXPERIMENTAL** library, use at your own risk
+
+This library is under heavy development and is in no way a final solution. This should only be used
+as a prototype or playground.
+
+# Module root
+
+androidx.lifecycle lifecycle-viewmodel-navigation3
+
+# Package androidx.lifecycle.viewmodel.navigation3
+
+Library that provides ViewModel extensions for the Navigation 3 library.
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt b/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
new file mode 100644
index 0000000..dde49dd
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.lifecycle.viewmodel.navigation3 {
+
+ public final class ViewModelStoreNavContentWrapper implements androidx.navigation3.NavContentWrapper {
+ method @androidx.compose.runtime.Composable public void WrapContent(androidx.navigation3.Record record);
+ field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper INSTANCE;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/api/res-current.txt b/lifecycle/lifecycle-viewmodel-navigation3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-navigation3/api/res-current.txt
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
new file mode 100644
index 0000000..dde49dd
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.lifecycle.viewmodel.navigation3 {
+
+ public final class ViewModelStoreNavContentWrapper implements androidx.navigation3.NavContentWrapper {
+ method @androidx.compose.runtime.Composable public void WrapContent(androidx.navigation3.Record record);
+ field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper INSTANCE;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/build.gradle b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
new file mode 100644
index 0000000..c270ddf
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+import androidx.build.Publish
+import androidx.build.PlatformIdentifier
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+}
+
+androidXMultiplatform {
+ android()
+
+ defaultPlatform(PlatformIdentifier.ANDROID)
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation("androidx.compose.runtime:runtime:1.7.5")
+ implementation("androidx.compose.runtime:runtime-saveable:1.7.5")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.8.7")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
+ }
+ }
+
+ commonTest {
+ dependencies {
+ implementation(libs.kotlinTest)
+ implementation(project(":kruth:kruth"))
+ implementation(project(":compose:runtime:runtime-test-utils"))
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation("androidx.compose.ui:ui:1.7.5")
+ implementation project(":navigation3:navigation3")
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidInstrumentedTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.testExtJunitKtx)
+ implementation(libs.truth)
+ implementation(project(":compose:test-utils"))
+ implementation("androidx.compose.ui:ui-test:1.7.5")
+ implementation("androidx.compose.ui:ui-test-junit4:1.7.5")
+ }
+ }
+ }
+}
+
+android {
+ compileSdk 35
+ namespace "androidx.lifecycle.viewmodel.navigation3"
+}
+
+androidx {
+ name = "Androidx Lifecycle Navigation3 ViewModel"
+ publish = Publish.SNAPSHOT_ONLY
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ description = "Provides the ViewModel wrapper for nav3."
+ doNotDocumentReason = "Not published to maven"
+}
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
new file mode 100644
index 0000000..7d38a31
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.lifecycle.viewmodel.navigation3
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.kruth.assertWithMessage
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation3.Record
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlin.test.Test
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ViewModelStoreNavContentWrapperTest {
+ @get:Rule val composeTestRule = createComposeRule()
+
+ @Test
+ fun testViewModelProvided() {
+ val wrapper = ViewModelStoreNavContentWrapper
+ lateinit var viewModel1: MyViewModel
+ lateinit var viewModel2: MyViewModel
+ val record1Arg = "record1 Arg"
+ val record2Arg = "record2 Arg"
+ val record1 =
+ Record("key1") {
+ viewModel1 = viewModel<MyViewModel>()
+ viewModel1.myArg = record1Arg
+ }
+ val record2 =
+ Record("key2") {
+ viewModel2 = viewModel<MyViewModel>()
+ viewModel2.myArg = record2Arg
+ }
+ composeTestRule.setContent {
+ wrapper.WrapContent(record1)
+ wrapper.WrapContent(record2)
+ }
+
+ composeTestRule.runOnIdle {
+ assertWithMessage("Incorrect arg for record 1")
+ .that(viewModel1.myArg)
+ .isEqualTo(record1Arg)
+ assertWithMessage("Incorrect arg for record 2")
+ .that(viewModel2.myArg)
+ .isEqualTo(record2Arg)
+ }
+ }
+}
+
+class MyViewModel : ViewModel() {
+ var myArg = "default"
+}
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
new file mode 100644
index 0000000..e319249
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.lifecycle.viewmodel.navigation3
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
+import androidx.lifecycle.HasDefaultViewModelProviderFactory
+import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
+import androidx.lifecycle.SavedStateViewModelFactory
+import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStore
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.MutableCreationExtras
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation3.NavContentWrapper
+import androidx.navigation3.Record
+
+/**
+ * Provides the content of a [Record] with a [ViewModelStoreOwner] and provides that
+ * [ViewModelStoreOwner] as a [LocalViewModelStoreOwner] so that it is available within the content.
+ */
+public object ViewModelStoreNavContentWrapper : NavContentWrapper {
+
+ @Composable
+ override fun WrapContent(record: Record) {
+ val key = record.key
+ val recordViewModelStoreProvider = viewModel { RecordViewModel() }
+ val viewModelStore = recordViewModelStoreProvider.viewModelStoreForKey(key)
+ // This ensures we always keep viewModels on config changes.
+ val activity = LocalContext.current.findActivity()
+ remember(key, viewModelStore) {
+ object : RememberObserver {
+ override fun onAbandoned() {
+ disposeIfNotChangingConfiguration()
+ }
+
+ override fun onForgotten() {
+ disposeIfNotChangingConfiguration()
+ }
+
+ override fun onRemembered() {}
+
+ fun disposeIfNotChangingConfiguration() {
+ if (activity?.isChangingConfigurations != true) {
+ recordViewModelStoreProvider.removeViewModelStoreOwnerForKey(key)?.clear()
+ }
+ }
+ }
+ }
+
+ val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
+ CompositionLocalProvider(
+ LocalViewModelStoreOwner provides
+ object : ViewModelStoreOwner, HasDefaultViewModelProviderFactory {
+ override val viewModelStore: ViewModelStore
+ get() = viewModelStore
+
+ override val defaultViewModelProviderFactory: ViewModelProvider.Factory
+ get() = SavedStateViewModelFactory(null, savedStateRegistryOwner)
+
+ override val defaultViewModelCreationExtras: CreationExtras
+ get() =
+ MutableCreationExtras().also {
+ it[SAVED_STATE_REGISTRY_OWNER_KEY] = savedStateRegistryOwner
+ it[VIEW_MODEL_STORE_OWNER_KEY] = this
+ }
+ }
+ ) {
+ record.content.invoke(key)
+ }
+ }
+}
+
+private class RecordViewModel : ViewModel() {
+ private val owners = mutableMapOf<Any, ViewModelStore>()
+
+ fun viewModelStoreForKey(key: Any): ViewModelStore = owners.getOrPut(key) { ViewModelStore() }
+
+ fun removeViewModelStoreOwnerForKey(key: Any): ViewModelStore? = owners.remove(key)
+
+ override fun onCleared() {
+ owners.forEach { (_, store) -> store.clear() }
+ }
+}
+
+private fun Context.findActivity(): Activity? {
+ var context = this
+ while (context is ContextWrapper) {
+ if (context is Activity) return context
+ context = context.baseContext
+ }
+ return null
+}
diff --git a/navigation3/navigation3/api/current.txt b/navigation3/navigation3/api/current.txt
index b0e1510..17c13b7 100644
--- a/navigation3/navigation3/api/current.txt
+++ b/navigation3/navigation3/api/current.txt
@@ -27,6 +27,12 @@
public final class Record {
ctor public Record(Object key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit> content);
+ method public kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit> getContent();
+ method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+ method public Object getKey();
+ property public final kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit> content;
+ property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ property public final Object key;
}
public final class SaveableStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
diff --git a/navigation3/navigation3/api/restricted_current.txt b/navigation3/navigation3/api/restricted_current.txt
index b0e1510..17c13b7 100644
--- a/navigation3/navigation3/api/restricted_current.txt
+++ b/navigation3/navigation3/api/restricted_current.txt
@@ -27,6 +27,12 @@
public final class Record {
ctor public Record(Object key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit> content);
+ method public kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit> getContent();
+ method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+ method public Object getKey();
+ property public final kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit> content;
+ property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ property public final Object key;
}
public final class SaveableStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
diff --git a/navigation3/navigation3/build.gradle b/navigation3/navigation3/build.gradle
index 361be2d..f61edaa 100644
--- a/navigation3/navigation3/build.gradle
+++ b/navigation3/navigation3/build.gradle
@@ -111,7 +111,7 @@
androidx {
name = "Androidx Navigation 3"
- publish = Publish.NONE
+ publish = Publish.SNAPSHOT_ONLY
type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
inceptionYear = "2024"
description = "Provides the building blocks for a Compose first Navigation solution that " +
diff --git a/navigation3/navigation3/samples/build.gradle b/navigation3/navigation3/samples/build.gradle
index 0576226..b2b8be8 100644
--- a/navigation3/navigation3/samples/build.gradle
+++ b/navigation3/navigation3/samples/build.gradle
@@ -43,7 +43,10 @@
implementation("androidx.compose.material3:material3:1.3.1")
implementation("androidx.compose.runtime:runtime:1.7.5")
implementation("androidx.compose.ui:ui:1.7.5")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.8.7")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation(libs.kotlinSerializationCore)
+ implementation project(":lifecycle:lifecycle-viewmodel-navigation3")
implementation project(":navigation3:navigation3")
}
diff --git a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
index 5158efa..f7adfa2 100644
--- a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
+++ b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
@@ -19,6 +19,9 @@
import androidx.annotation.Sampled
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper
import androidx.navigation3.NavDisplay
import androidx.navigation3.Record
import androidx.navigation3.SavedStateNavContentWrapper
@@ -28,7 +31,10 @@
@Composable
fun BasicNav() {
val backStack = rememberMutableStateListOf(Profile)
- val manager = rememberNavWrapperManager(listOf(SavedStateNavContentWrapper))
+ val manager =
+ rememberNavWrapperManager(
+ listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
+ )
NavDisplay(
backstack = backStack,
wrapperManager = manager,
@@ -36,7 +42,10 @@
) { key ->
when (key) {
Profile -> {
- Record(Profile) { Profile({ backStack.add(it) }) { backStack.removeLast() } }
+ Record(Profile) {
+ val viewModel = viewModel<ProfileViewModel>()
+ Profile(viewModel, { backStack.add(it) }) { backStack.removeLast() }
+ }
}
Scrollable -> {
Record(Scrollable) { Scrollable({ backStack.add(it) }) { backStack.removeLast() } }
@@ -58,3 +67,7 @@
}
}
}
+
+class ProfileViewModel : ViewModel() {
+ val name = "no user"
+}
diff --git a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavigationSamples.kt b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavigationSamples.kt
index 55ea9eb1..f688908 100644
--- a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavigationSamples.kt
+++ b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavigationSamples.kt
@@ -65,9 +65,9 @@
}
@Composable
-fun Profile(navigateTo: (Any) -> Unit, onBack: () -> Unit) {
+fun Profile(viewModel: ProfileViewModel, navigateTo: (Any) -> Unit, onBack: () -> Unit) {
Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
- Text(text = stringResource(Profile.resourceId))
+ Text(text = "${viewModel.name} ${stringResource(Profile.resourceId)}")
NavigateButton(stringResource(Dashboard.resourceId)) { navigateTo(Dashboard()) }
Divider(color = Color.Black)
NavigateButton(stringResource(Scrollable.resourceId)) { navigateTo(Scrollable) }
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/Record.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/Record.kt
index 5c378c6..607e6ba 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/Record.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/Record.kt
@@ -27,7 +27,7 @@
* @param content content for this record to be displayed when this record is active
*/
public class Record(
- internal val key: Any,
- internal val featureMap: Map<String, Any> = emptyMap(),
- internal val content: @Composable (Any) -> Unit,
+ public val key: Any,
+ public val featureMap: Map<String, Any> = emptyMap(),
+ public val content: @Composable (Any) -> Unit,
)
diff --git a/settings.gradle b/settings.gradle
index 50266ac..89583eb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -801,6 +801,7 @@
includeProject(":lifecycle:lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.KMP])
includeProject(":lifecycle:lifecycle-viewmodel-savedstate-samples", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.KMP])
includeProject(":lifecycle:lifecycle-viewmodel-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.INFRAROGUE, BuildType.KMP])
+includeProject(":lifecycle:lifecycle-viewmodel-navigation3", [BuildType.COMPOSE])
includeProject(":lint:lint-gradle", [BuildType.MAIN])
includeProject(":lint-checks")
includeProject(":lint-checks:integration-tests")