diff --git a/library/compose/build.gradle.kts b/library/compose/build.gradle.kts index f67dfe3..b42c0a9 100644 --- a/library/compose/build.gradle.kts +++ b/library/compose/build.gradle.kts @@ -45,15 +45,15 @@ kotlin { implementation(libs.jetbrains.lifecycle.viewmodel) implementation(libs.jetbrains.lifecycle.viewmodel.compose) - - implementation(libs.androidx.navigation3) +// implementation(libs.androidx.navigation3) implementation(libs.jetbrains.savedstate.compose) } } androidMain { dependencies { - implementation(libs.androidx.viewmodel.navigation3) + implementation(libs.androidx.activity.compose) +// implementation(libs.androidx.viewmodel.navigation3) } } diff --git a/library/compose/src/androidMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.android.kt b/library/compose/src/androidMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.android.kt new file mode 100644 index 0000000..cad6e06 --- /dev/null +++ b/library/compose/src/androidMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.android.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2021 Google LLC + * + * 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 com.tunjid.treenav.compose.navigation3.decorators + +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.lifecycle.HasDefaultViewModelProviderFactory +import androidx.lifecycle.Lifecycle +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.enableSavedStateHandles +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.compose.LocalSavedStateRegistryOwner +import com.tunjid.treenav.compose.navigation3.NavEntry +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator + +/** + * Provides the content of a [NavEntry] with a [ViewModelStoreOwner] and provides that + * [ViewModelStoreOwner] as a [LocalViewModelStoreOwner] so that it is available within the content. + * + * This requires that usage of the [SavedStateNavEntryDecorator] to ensure that the [NavEntry] + * scoped [ViewModel]s can properly provide access to [SavedStateHandle]s + */ +internal object AndroidViewModelStoreNavEntryDecorator : NavEntryDecorator { + + @Composable + override fun DecorateBackStack(backStack: List, content: @Composable () -> Unit) { + val entryViewModelStoreProvider = viewModel { EntryViewModel() } + entryViewModelStoreProvider.ownerInBackStack.clear() + entryViewModelStoreProvider.ownerInBackStack.addAll(backStack) + val localInfo = remember { ViewModelStoreNavLocalInfo() } + DisposableEffect(key1 = backStack) { onDispose { localInfo.refCount.clear() } } + + val activity = LocalActivity.current + backStack.forEachIndexed { index, key -> + // We update here as part of composition to ensure the value is available to + // DecorateEntry + localInfo.refCount.getOrPut(key) { LinkedHashSet() }.add(getIdForKey(key, index)) + DisposableEffect(key1 = key) { + localInfo.refCount + .getOrPut(key) { LinkedHashSet() } + .add(getIdForKey(key, index)) + onDispose { + // If the backStack count is less than the refCount for the key, remove the + // state since that means we removed a key from the backstack, and set the + // refCount to the backstack count. + val backstackCount = backStack.count { it == key } + val lastKeyCount = localInfo.refCount[key]?.size ?: 0 + if (backstackCount < lastKeyCount) { + // The set of the ids associated with this key + @Suppress("PrimitiveInCollection") // The order of the element matters + val idsSet = localInfo.refCount[key]!! + val id = idsSet.last() + idsSet.remove(id) + if (!localInfo.idsInComposition.contains(id)) { + if (activity?.isChangingConfigurations != true) { + entryViewModelStoreProvider + .removeViewModelStoreOwnerForKey(id) + ?.clear() + } + } + } + + // If the refCount is 0, remove the key from the refCount. + if (localInfo.refCount[key]?.isEmpty() == true) { + localInfo.refCount.remove(key) + } + } + } + } + + CompositionLocalProvider(LocalViewModelStoreNavLocalInfo provides localInfo) { + content.invoke() + } + } + + @Composable + override fun DecorateEntry(entry: NavEntry) { + val key = entry.key + val entryViewModelStoreProvider = viewModel { EntryViewModel() } + + val activity = LocalActivity.current + val localInfo = LocalViewModelStoreNavLocalInfo.current + // Tracks whether the key is changed + var keyChanged = false + var id: Int = + rememberSaveable(key) { + keyChanged = true + localInfo.refCount[key]!!.last() + } + id = + rememberSaveable(localInfo.refCount[key]?.size) { + // if the key changed, use the current id + // If the key was not changed, and the current id is not in composition or on the + // back stack then update the id with the last item from the backstack with the + // associated key. This ensures that we can handle duplicates, both consecutive and + // non-consecutive + if ( + !keyChanged && + (!localInfo.idsInComposition.contains(id) || + localInfo.refCount[key]?.contains(id) == true) + ) { + localInfo.refCount[key]!!.last() + } else { + id + } + } + keyChanged = false + + val viewModelStore = entryViewModelStoreProvider.viewModelStoreForKey(id) + + DisposableEffect(key1 = key) { + localInfo.idsInComposition.add(id) + onDispose { + if (localInfo.idsInComposition.remove(id) && !localInfo.refCount.contains(key)) { + if (activity?.isChangingConfigurations != true) { + entryViewModelStoreProvider.removeViewModelStoreOwnerForKey(id)?.clear() + } + // If the refCount is 0, remove the key from the refCount. + if (localInfo.refCount[key]?.isEmpty() == true) { + localInfo.refCount.remove(key) + } + } + } + } + + val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current + val childViewModelOwner = remember { + object : + ViewModelStoreOwner, + SavedStateRegistryOwner by savedStateRegistryOwner, + 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 + } + + init { + require(this.lifecycle.currentState == Lifecycle.State.INITIALIZED) { + "The Lifecycle state is already beyond INITIALIZED. The " + + "ViewModelStoreNavEntryDecorator requires adding the " + + "SavedStateNavEntryDecorator to ensure support for " + + "SavedStateHandles." + } + enableSavedStateHandles() + } + } + } + CompositionLocalProvider(LocalViewModelStoreOwner provides childViewModelOwner) { + entry.content.invoke(key) + } + } +} + +@Stable +internal actual val ViewModelStoreNavEntryDecorator: NavEntryDecorator + get() = AndroidViewModelStoreNavEntryDecorator \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.kt index ece7241..1cf35b5 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.kt @@ -36,14 +36,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf -import androidx.navigation3.DecoratedNavEntryProvider -import androidx.navigation3.NavEntry -import androidx.navigation3.NavEntryDecorator -import androidx.navigation3.SaveableStateNavEntryDecorator -import androidx.navigation3.SavedStateNavEntryDecorator import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.decorators.DefaultViewModelStoreNavEntryDecorator -import com.tunjid.treenav.compose.decorators.TransitionAwareLifecycleNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.DecoratedNavEntryProvider +import com.tunjid.treenav.compose.navigation3.NavEntry +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.DefaultViewModelStoreNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.SaveableStateNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.SavedStateNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.TransitionAwareLifecycleNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.ViewModelStoreNavEntryDecorator @Composable internal fun DecoratedNavEntryMultiPaneDisplayScope( @@ -81,7 +82,7 @@ internal fun DecoratedNavEntr SaveableStateNavEntryDecorator, SavedStateNavEntryDecorator, transitionAwareLifecycleNavEntryDecorator, - CastPlatformViewModelStoreNavEntryDecorator, + ViewModelStoreNavEntryDecorator, ), content = { entries -> val updatedEntries by rememberUpdatedState(entries) @@ -233,11 +234,4 @@ private val LocalPaneScope = staticCompositionLocalOf> { ) } -@Stable -private val CastPlatformViewModelStoreNavEntryDecorator: NavEntryDecorator - get() = PlatformViewModelStoreNavEntryDecorator as? NavEntryDecorator - ?: DefaultViewModelStoreNavEntryDecorator - -@Stable -internal expect val PlatformViewModelStoreNavEntryDecorator: Any? diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/DecoratedNavEntryProvider.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/DecoratedNavEntryProvider.kt new file mode 100644 index 0000000..c63738c --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/DecoratedNavEntryProvider.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Google LLC + * + * 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 com.tunjid.treenav.compose.navigation3 + +import androidx.compose.runtime.Composable + +/** + * Function that provides all of the [NavEntry]s wrapped with the given [NavEntryDecorator]s. It is + * responsible for executing the functions provided by each [NavEntryDecorator] appropriately. + * + * Note: the order in which the [NavEntryDecorator]s are added to the list determines their scope, + * i.e. a [NavEntryDecorator] added earlier in a list has its data available to those added later. + * + * @param backStack the list of keys that represent the backstack + * @param entryDecorators the [NavEntryDecorator]s that are providing data to the content + * @param entryProvider a function that returns the [NavEntry] for a given key + * @param content the content to be displayed + */ +@Composable +internal fun DecoratedNavEntryProvider( + backStack: List, + entryProvider: (key: T) -> NavEntry, + entryDecorators: List, + content: @Composable (List>) -> Unit, +) { + // Kotlin does not know these things are compatible so we need this explicit cast + // to ensure our lambda below takes the correct type + entryProvider as (T) -> NavEntry + + // Generates a list of entries that are wrapped with the given providers + val entries = + backStack.map { + val entry = entryProvider.invoke(it) + entryDecorators.distinct().foldRight(entry) { + provider: NavEntryDecorator, wrappedEntry, + -> + object : NavEntryWrapper(wrappedEntry) { + override val content: @Composable ((T) -> Unit) = { + provider.DecorateEntry(wrappedEntry) + } + } + } + } + + // Provides the entire backstack to the previously wrapped entries + entryDecorators + .distinct() + .foldRight Unit>({ content(entries) }) { + provider: NavEntryDecorator, + wrappedContent, + -> + { provider.DecorateBackStack(backStack = backStack, wrappedContent) } + } + .invoke() +} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntry.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntry.kt new file mode 100644 index 0000000..9416ac9 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntry.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * + * 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 com.tunjid.treenav.compose.navigation3 +import androidx.compose.runtime.Composable + +/** + * Entry maintains and stores the key and the content represented by that key. Entries should be + * created as part of a [NavDisplay.entryProvider](reference/androidx/navigation/NavDisplay). + * + * @param key key for this entry + * @param metadata provides information to the display + * @param content content for this entry to be displayed when this entry is active + */ +internal open class NavEntry( + public open val key: T, + public open val metadata: Map = emptyMap(), + public open val content: @Composable (T) -> Unit +) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryDecorator.kt new file mode 100644 index 0000000..8897275 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryDecorator.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Google LLC + * + * 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 com.tunjid.treenav.compose.navigation3 + +import androidx.compose.runtime.Composable + +/** + * Interface that offers the ability to provide information to some Composable content that is + * integrated with a [NavDisplay](reference/androidx/navigation/NavDisplay). + * + * Information can be provided to the entire back stack via [NavEntryDecorator.DecorateBackStack] or + * to a single entry via [NavEntryDecorator.DecorateEntry]. + */ +internal interface NavEntryDecorator { + + /** + * Allows a [NavEntryDecorator] to provide to the entire backstack. + * + * This function is called by the [DecoratedNavEntryProvider] and should not be called directly. + */ + @Composable + public fun DecorateBackStack(backStack: List, content: @Composable () -> Unit): Unit = + content.invoke() + + /** + * Allows a [NavEntryDecorator] to provide information to a single entry. + * + * This function is called by the [NavDisplay](reference/androidx/navigation/NavDisplay) and + * should not be called directly. + */ + @Composable public fun DecorateEntry(entry: NavEntry) +} diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryWrapper.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryWrapper.kt new file mode 100644 index 0000000..35a8d35 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryWrapper.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * 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 com.tunjid.treenav.compose.navigation3 + +import androidx.compose.runtime.Composable + +/** + * Class that wraps a [NavEntry] within another [NavEntry]. + * + * This provides a nesting mechanism for [NavEntry]s that allows properly nested content. + * + * @param navEntry the [NavEntry] to wrap + */ +internal open class NavEntryWrapper(public val navEntry: NavEntry) : + NavEntry(navEntry.key, navEntry.metadata, navEntry.content) { + override val key: T + get() = navEntry.key + + override val metadata: Map + get() = navEntry.metadata + + override val content: @Composable (T) -> Unit + get() = navEntry.content +} diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SaveableStateNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SaveableStateNavEntryDecorator.kt new file mode 100644 index 0000000..36d4644 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SaveableStateNavEntryDecorator.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2021 Google LLC + * + * 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 com.tunjid.treenav.compose.navigation3.decorators + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.SaveableStateHolder +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.runtime.staticCompositionLocalOf +import com.tunjid.treenav.compose.navigation3.NavEntry +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator +import kotlin.collections.LinkedHashSet + +/** + * Wraps the content of a [NavEntry] with a [SaveableStateHolder.SaveableStateProvider] to ensure + * that calls to [rememberSaveable] within the content work properly and that state can be saved. + * + * This [NavEntryDecorator] is the only one that is **required** as saving state is considered a + * non-optional feature. + */ +internal object SaveableStateNavEntryDecorator : NavEntryDecorator { + + @Composable + override fun DecorateBackStack(backStack: List, content: @Composable () -> Unit) { + val localInfo = remember { SaveableStateNavLocalInfo() } + DisposableEffect(key1 = backStack) { onDispose { localInfo.refCount.clear() } } + + localInfo.savedStateHolder = rememberSaveableStateHolder() + backStack.forEachIndexed { index, key -> + // We update here as part of composition to ensure the value is available to + // DecorateEntry + localInfo.refCount.getOrPut(key) { LinkedHashSet() }.add(getIdForKey(key, index)) + DisposableEffect(key1 = key) { + // We update here at the end of composition in case the backstack changed and + // everything was cleared. + localInfo.refCount + .getOrPut(key) { LinkedHashSet() } + .add(getIdForKey(key, index)) + onDispose { + // If the backStack count is less than the refCount for the key, remove the + // state since that means we removed a key from the backstack, and set the + // refCount to the backstack count. + val backstackCount = backStack.count { it == key } + val lastKeyCount = localInfo.refCount[key]?.size ?: 0 + if (backstackCount < lastKeyCount) { + // The set of the ids associated with this key + @Suppress("PrimitiveInCollection") // The order of the element matters + val idsSet = localInfo.refCount[key]!! + val id = idsSet.last() + idsSet.remove(id) + if (!localInfo.idsInComposition.contains(id)) { + localInfo.savedStateHolder!!.removeState(id) + } + } + // If the refCount is 0, remove the key from the refCount. + if (localInfo.refCount[key]?.isEmpty() == true) { + localInfo.refCount.remove(key) + } + } + } + } + + CompositionLocalProvider(LocalSaveableStateNavLocalInfo provides localInfo) { + content.invoke() + } + } + + @Composable + public override fun DecorateEntry(entry: NavEntry) { + val localInfo = LocalSaveableStateNavLocalInfo.current + val key = entry.key + // Tracks whether the key is changed + var keyChanged = false + var id: Int = + rememberSaveable(key) { + keyChanged = true + localInfo.refCount[key]!!.last() + } + id = + rememberSaveable(localInfo.refCount[key]?.size) { + // if the key changed, use the current id + // If the key was not changed, and the current id is not in composition or on the + // back + // stack then update the id with the last item from the backstack with the + // associated + // key. This ensures that we can handle duplicates, both consecutive and + // non-consecutive + if ( + !keyChanged && + (!localInfo.idsInComposition.contains(id) || + localInfo.refCount[key]?.contains(id) == true) + ) { + localInfo.refCount[key]!!.last() + } else { + id + } + } + keyChanged = false + DisposableEffect(key1 = key) { + localInfo.idsInComposition.add(id) + onDispose { + if (localInfo.idsInComposition.remove(id) && !localInfo.refCount.contains(key)) { + localInfo.savedStateHolder!!.removeState(id) + // If the refCount is 0, remove the key from the refCount. + if (localInfo.refCount[key]?.isEmpty() == true) { + localInfo.refCount.remove(key) + } + } + } + } + + localInfo.savedStateHolder?.SaveableStateProvider(id) { entry.content.invoke(key) } + } +} + +internal val LocalSaveableStateNavLocalInfo = + staticCompositionLocalOf { + error( + "CompositionLocal LocalSaveableStateNavLocalInfo not present. You must call " + + "DecorateBackStack before calling DecorateEntry." + ) + } + +internal class SaveableStateNavLocalInfo { + internal var savedStateHolder: SaveableStateHolder? = null + internal val refCount: MutableMap> = mutableMapOf() + @Suppress("PrimitiveInCollection") // The order of the element matters + internal val idsInComposition: LinkedHashSet = LinkedHashSet() +} diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SavedStateNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SavedStateNavEntryDecorator.kt new file mode 100644 index 0000000..1d93398 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SavedStateNavEntryDecorator.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Google LLC + * + * 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 com.tunjid.treenav.compose.navigation3.decorators + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleRegistry +import androidx.savedstate.SavedState +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.compose.LocalSavedStateRegistryOwner +import androidx.savedstate.savedState +import com.tunjid.treenav.compose.navigation3.NavEntry +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator + + +/** + * Provides the content of a [NavEntry] with a [SavedStateRegistryOwner] and provides that + * [SavedStateRegistryOwner] as a [LocalSavedStateRegistryOwner] so that it is available within the + * content. + */ +internal object SavedStateNavEntryDecorator : NavEntryDecorator { + + @Composable + override fun DecorateEntry(entry: NavEntry) { + val key = entry.key + val childRegistry by + rememberSaveable( + key, + stateSaver = + Saver( + save = { it.savedState }, + restore = { EntrySavedStateRegistry().apply { savedState = it } } + ) + ) { + mutableStateOf(EntrySavedStateRegistry()) + } + + CompositionLocalProvider(LocalSavedStateRegistryOwner provides childRegistry) { + entry.content.invoke(key) + } + + DisposableEffect(key1 = key) { + childRegistry.lifecycle.currentState = Lifecycle.State.RESUMED + onDispose { + val savedState = savedState() + childRegistry.savedStateRegistryController.performSave(savedState) + childRegistry.savedState = savedState + childRegistry.lifecycle.currentState = Lifecycle.State.DESTROYED + } + } + } +} + +private class EntrySavedStateRegistry : SavedStateRegistryOwner { + override val lifecycle: LifecycleRegistry = LifecycleRegistry(this) + val savedStateRegistryController = SavedStateRegistryController.create(this) + override val savedStateRegistry: SavedStateRegistry = + savedStateRegistryController.savedStateRegistry + + var savedState: SavedState? = null + + init { + savedStateRegistryController.performRestore(savedState) + } +} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/decorators/TransitionAwareLifecycleNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt similarity index 96% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/decorators/TransitionAwareLifecycleNavEntryDecorator.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt index 1e1a849..d505400 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/decorators/TransitionAwareLifecycleNavEntryDecorator.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.tunjid.treenav.compose.decorators +package com.tunjid.treenav.compose.navigation3.decorators import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -30,8 +30,8 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.navigation3.NavEntry -import androidx.navigation3.NavEntryDecorator +import com.tunjid.treenav.compose.navigation3.NavEntry +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator internal class TransitionAwareLifecycleNavEntryDecorator : NavEntryDecorator { diff --git a/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.jvm.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/Utilities.kt similarity index 78% rename from library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.jvm.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/Utilities.kt index 20d608b..318a24f 100644 --- a/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.jvm.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/Utilities.kt @@ -14,10 +14,6 @@ * limitations under the License. */ -package com.tunjid.treenav.compose +package com.tunjid.treenav.compose.navigation3.decorators -import androidx.compose.runtime.Stable - -@Stable -internal actual val PlatformViewModelStoreNavEntryDecorator: Any? - get() = null \ No newline at end of file +internal fun getIdForKey(key: Any, count: Int): Int = 31 * key.hashCode() + count diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/decorators/DefaultViewModelStoreNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.kt similarity index 96% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/decorators/DefaultViewModelStoreNavEntryDecorator.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.kt index e24e6e2..adc68a3 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/decorators/DefaultViewModelStoreNavEntryDecorator.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.kt @@ -14,11 +14,12 @@ * limitations under the License. */ -package com.tunjid.treenav.compose.decorators +package com.tunjid.treenav.compose.navigation3.decorators import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.staticCompositionLocalOf @@ -36,10 +37,13 @@ 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.NavEntry -import androidx.navigation3.NavEntryDecorator import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.compose.LocalSavedStateRegistryOwner +import com.tunjid.treenav.compose.navigation3.NavEntry +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator + +@Stable +internal expect val ViewModelStoreNavEntryDecorator: NavEntryDecorator /** * Provides the content of a [NavEntry] with a [ViewModelStoreOwner] and provides that @@ -187,7 +191,7 @@ internal object DefaultViewModelStoreNavEntryDecorator : NavEntryDecorator { } } -private class EntryViewModel : ViewModel() { +internal class EntryViewModel : ViewModel() { private val owners = mutableMapOf() val ownerInBackStack = mutableListOf() @@ -213,5 +217,3 @@ internal class ViewModelStoreNavLocalInfo { @Suppress("PrimitiveInCollection") // The order of the element matters internal val idsInComposition: LinkedHashSet = LinkedHashSet() } - -internal fun getIdForKey(key: Any, count: Int): Int = 31 * key.hashCode() + count \ No newline at end of file diff --git a/library/compose/src/androidMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.android.kt b/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.kt similarity index 72% rename from library/compose/src/androidMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.android.kt rename to library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.kt index 01301ca..adbfd92 100644 --- a/library/compose/src/androidMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.android.kt +++ b/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.tunjid.treenav.compose +package com.tunjid.treenav.compose.navigation3.decorators import androidx.compose.runtime.Stable -import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator @Stable -internal actual val PlatformViewModelStoreNavEntryDecorator: Any? - get() = ViewModelStoreNavEntryDecorator \ No newline at end of file +internal actual val ViewModelStoreNavEntryDecorator: NavEntryDecorator + get() = DefaultViewModelStoreNavEntryDecorator \ No newline at end of file diff --git a/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.native.kt b/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.kt similarity index 72% rename from library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.native.kt rename to library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.kt index 20d608b..adbfd92 100644 --- a/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/DecoratedNavEntryMultiPaneDisplayScope.native.kt +++ b/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.kt @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.tunjid.treenav.compose +package com.tunjid.treenav.compose.navigation3.decorators import androidx.compose.runtime.Stable +import com.tunjid.treenav.compose.navigation3.NavEntryDecorator @Stable -internal actual val PlatformViewModelStoreNavEntryDecorator: Any? - get() = null \ No newline at end of file +internal actual val ViewModelStoreNavEntryDecorator: NavEntryDecorator + get() = DefaultViewModelStoreNavEntryDecorator \ No newline at end of file