From 5e3db542514d45f09739f3163fc74f0d0e937bc5 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 16:45:39 -0400 Subject: [PATCH 1/2] Updates to match the latest nav 3 --- gradle/libs.versions.toml | 4 +- ...ViewModelStoreNavEntryDecorator.android.kt | 178 +--------------- .../DecoratedNavEntryMultiPaneDisplayScope.kt | 21 +- .../navigation3/DecoratedNavEntryProvider.kt | 197 ++++++++++++++++-- .../compose/navigation3/NavEntryDecorator.kt | 64 ++++-- .../MovableContentNavEntryDecorator.kt | 108 ++++------ .../SaveableStateNavEntryDecorator.kt | 145 ------------- .../decorators/SavedStateNavEntryDecorator.kt | 71 +++++-- ...ansitionAwareLifecycleNavEntryDecorator.kt | 40 +--- .../ViewModelStoreNavEntryDecorator.kt | 190 ++++++----------- .../ViewModelStoreNavEntryDecorator.jvm.kt | 16 +- .../ViewModelStoreNavEntryDecorator.native.kt | 16 +- 12 files changed, 422 insertions(+), 628 deletions(-) delete mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SaveableStateNavEntryDecorator.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 138c8e3..bf60892 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,8 +14,8 @@ androidxTestExt = "1.2.1" androidxTestRunner = "1.6.2" androidxTestRules = "1.6.1" dokka = "1.8.20" -jetbrainsCompose = "1.8.0-rc01" -jetbrainsLifecycle = "2.9.0-alpha07" +jetbrainsCompose = "1.8.0" +jetbrainsLifecycle = "2.9.0-beta01" jetbrainsMaterial3Adaptive = "1.0.1" junit4 = "4.13.2" kotlin = "2.1.20" 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 index cad6e06..a1beb05 100644 --- 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 @@ -18,176 +18,18 @@ 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) - } - } +@Composable +internal actual fun shouldRemoveViewModelStoreCallback(): () -> Boolean { + val activity = LocalActivity.current + return { activity?.isChangingConfigurations != true } } -@Stable -internal actual val ViewModelStoreNavEntryDecorator: NavEntryDecorator - get() = AndroidViewModelStoreNavEntryDecorator \ No newline at end of file +internal actual fun SavedStateViewModelFactory( + savedStateRegistryOwner: SavedStateRegistryOwner +): SavedStateViewModelFactory = SavedStateViewModelFactory( + null, + savedStateRegistryOwner +) \ 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 6809e24..6a53b5c 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 @@ -39,11 +39,10 @@ import androidx.compose.runtime.staticCompositionLocalOf import com.tunjid.treenav.Node import com.tunjid.treenav.compose.navigation3.DecoratedNavEntryProvider import com.tunjid.treenav.compose.navigation3.NavEntry -import com.tunjid.treenav.compose.navigation3.decorators.MovableContentNavEntryDecorator -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 +import com.tunjid.treenav.compose.navigation3.decorators.rememberMovableContentNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.rememberSavedStateNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.rememberViewModelStoreNavEntryDecorator +import com.tunjid.treenav.compose.navigation3.decorators.transitionAwareLifecycleNavEntryDecorator @Composable internal fun DecoratedNavEntryMultiPaneDisplayScope( @@ -61,9 +60,8 @@ internal fun DecoratedNavEntr state.destinationTransform(navigationState) ) - val transitionAwareLifecycleNavEntryDecorator = remember { - TransitionAwareLifecycleNavEntryDecorator() - } + val transitionAwareLifecycleNavEntryDecorator = + transitionAwareLifecycleNavEntryDecorator(backStack, true) DecoratedNavEntryProvider( backStack = backStack, @@ -78,11 +76,10 @@ internal fun DecoratedNavEntr ) }, entryDecorators = listOf( - MovableContentNavEntryDecorator, - SaveableStateNavEntryDecorator, - SavedStateNavEntryDecorator, + rememberMovableContentNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), transitionAwareLifecycleNavEntryDecorator, - ViewModelStoreNavEntryDecorator, + rememberViewModelStoreNavEntryDecorator(), ), content = { entries -> val updatedEntries by rememberUpdatedState(entries) 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 index c63738c..497c500 100644 --- 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 @@ -16,7 +16,16 @@ package com.tunjid.treenav.compose.navigation3 + import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.staticCompositionLocalOf +import com.tunjid.treenav.compose.navigation3.decorators.rememberSavedStateNavEntryDecorator +import kotlin.jvm.JvmSuppressWildcards /** * Function that provides all of the [NavEntry]s wrapped with the given [NavEntryDecorator]s. It is @@ -34,8 +43,9 @@ import androidx.compose.runtime.Composable internal fun DecoratedNavEntryProvider( backStack: List, entryProvider: (key: T) -> NavEntry, - entryDecorators: List, - content: @Composable (List>) -> Unit, + entryDecorators: List<@JvmSuppressWildcards NavEntryDecorator<*>> = + listOf(rememberSavedStateNavEntryDecorator()), + 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 @@ -45,25 +55,176 @@ internal fun DecoratedNavEntryProvider( 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) + decorateEntry(entry, entryDecorators as List>) + } + + // Provides the entire backstack to the previously wrapped entries + val initial: @Composable () -> Unit = remember(entries) { { content(entries) } } + + PrepareBackStack(backStack, entryDecorators, initial) +} + +/** + * Wraps a [NavEntry] with the list of [NavEntryDecorator] in the order that the decorators were + * added to the list. + * + * Invokes pop callback for popped entries that had pop animations and thus could not be cleaned up + * by [PrepareBackStack]. PrepareBackStack has no access to animation state so we rely on this + * function to call onPop when animation finishes. + */ +@Composable +internal fun decorateEntry( + entry: NavEntry, + decorators: List>, +): NavEntry { + val initial = + object : NavEntryWrapper(entry) { + override val content: @Composable ((T) -> Unit) = { + val key = entry.key + // Tracks whether the key is changed + var keyChanged = false + val localInfo = LocalNavEntryDecoratorLocalInfo.current + val keyIds = localInfo.keyIds[key] + val lastId = keyIds!!.last() + var id: Int = + rememberSaveable(keyIds.last()) { + keyChanged = true + lastId + } + id = + rememberSaveable(keyIds.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) || keyIds.contains(id)) + ) { + lastId + } else { + id + } + } + + keyChanged = false + + // store onPop for every decorator that has ever decorated this entry + // so that onPop will be called for newly added or removed decorators as well + val popCallbacks = remember { LinkedHashSet<(Any) -> Unit>() } + + DisposableEffect(key1 = key) { + localInfo.idsInComposition.add(id) + onDispose { + val notInComposition = localInfo.idsInComposition.remove(id) + val popped = !localInfo.keyIds.contains(key) + if (notInComposition && popped) { + // we reverse the scopes before popping to imitate the order + // of onDispose calls if each scope/decorator had their own + // onDispose + // calls for clean up + // convert to mutableList first for backwards compat. + popCallbacks.toMutableList().reversed().forEach { it(key) } + // If the refCount is 0, remove the key from the refCount. + if (localInfo.keyIds[key]?.isEmpty() == true) { + localInfo.keyIds.remove(key) + } + } } } + decorators.distinct().forEach { decorator -> popCallbacks.add(decorator.onPop) } + DecorateNavEntry(entry, decorators) } } + return initial +} - // Provides the entire backstack to the previously wrapped entries - entryDecorators - .distinct() - .foldRight Unit>({ content(entries) }) { - provider: NavEntryDecorator, - wrappedContent, - -> - { provider.DecorateBackStack(backStack = backStack, wrappedContent) } +/** + * Sets up logic to track changes to the backstack and invokes the [DecoratedNavEntryProvider] + * content. + * + * Invokes pop callback for popped entries that: + * 1. are not animating (i.e. no pop animations) AND / OR + * 2. have never been composed (i.e. never invoked with [DecorateNavEntry]) + */ +@Composable +internal fun PrepareBackStack( + backStack: List, + decorators: List>, + content: @Composable (() -> Unit), +) { + val localInfo = remember { NavEntryDecoratorLocalInfo() } + + DisposableEffect(key1 = backStack) { onDispose { localInfo.keyIds.clear() } } + + backStack.forEachIndexed { index, key -> + val id = getIdForEntry(key, index) + localInfo.keyIds.getOrPut(key) { LinkedHashSet() }.add(id) + + // store onPop for every decorator has ever decorated this key + // so that onPop will be called for newly added or removed decorators as well + val popCallbacks = remember(key) { LinkedHashSet<(Any) -> Unit>() } + decorators.distinct().forEach { popCallbacks.add(it.onPop) } + + key(key) { + DisposableEffect(key) { + // We update here as part of composition to ensure the value is available to + // ProvideToEntry + localInfo.keyIds.getOrPut(key) { LinkedHashSet() }.add(id) + 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.keyIds[key]?.size ?: 0 + if (backstackCount < lastKeyCount) { + // if popped, remove id from set of ids for this key + localInfo.keyIds[key]!!.remove(id) + // run onPop callback + if (!localInfo.idsInComposition.contains(id)) { + // we reverse the order before popping to imitate the order + // of onDispose calls if each scope/decorator had their own onDispose + // calls for clean up. convert to mutableList first for backwards compat. + popCallbacks.toMutableList().reversed().forEach { it(key) } + } + } + // If the refCount is 0, remove the key from the refCount. + if (localInfo.keyIds[key]?.isEmpty() == true) { + localInfo.keyIds.remove(key) + } + } + } + } + } + CompositionLocalProvider(LocalNavEntryDecoratorLocalInfo provides localInfo) { content() } +} + +private class NavEntryDecoratorLocalInfo { + val keyIds: MutableMap> = mutableMapOf() + + @Suppress("PrimitiveInCollection") // The order of the element matters + val idsInComposition: LinkedHashSet = LinkedHashSet() + val popCallbacks: LinkedHashMap Unit> = LinkedHashMap() + + fun populatePopMap(decorators: List>) { + decorators.reversed().forEach { decorator -> + popCallbacks.getOrPut(decorator.hashCode(), decorator::onPop) } - .invoke() -} \ No newline at end of file + } +} + +private val LocalNavEntryDecoratorLocalInfo = + staticCompositionLocalOf { + error( + "CompositionLocal LocalProviderLocalInfo not present. You must call " + + "ProvideToBackStack before calling ProvideToEntry." + ) + } + +private fun getIdForEntry(key: Any, count: Int): Int = 31 * key.hashCode() + count \ No newline at end of file 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 index 8897275..543daf8 100644 --- 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 @@ -17,30 +17,50 @@ package com.tunjid.treenav.compose.navigation3 import androidx.compose.runtime.Composable +import kotlin.jvm.JvmSuppressWildcards + +/** Marker class to hold the onPop and decorator functions that will be invoked at runtime. */ +internal class NavEntryDecorator +internal constructor( + internal val onPop: (key: Any) -> Unit, + internal val navEntryDecorator: @Composable (entry: NavEntry) -> Unit +) /** - * Interface that offers the ability to provide information to some Composable content that is - * integrated with a [NavDisplay](reference/androidx/navigation/NavDisplay). + * Function to provide information to all the [NavEntry] that are integrated with a + * [DecoratedNavEntryProvider]. * - * Information can be provided to the entire back stack via [NavEntryDecorator.DecorateBackStack] or - * to a single entry via [NavEntryDecorator.DecorateEntry]. + * @param onPop a callback that provides the key of a [NavEntry] that has been popped from the + * backStack and is leaving composition. This optional callback should to be used to clean up + * states that were used to decorate the NavEntry3 + * @param decorator the composable function to provide information to a [NavEntry] [decorator]. Note + * that this function only gets invoked for NavEntries that are actually getting rendered (i.e. by + * invoking the [NavEntry.content].) */ -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() +internal fun navEntryDecorator( + onPop: (key: Any) -> Unit = {}, + decorator: @Composable (entry: NavEntry) -> Unit +): NavEntryDecorator = NavEntryDecorator(onPop, decorator) - /** - * 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) -} +/** + * Wraps a [NavEntry] with the list of [NavEntryDecorator] in the order that the decorators were + * added to the list and invokes the content of the wrapped entry. + */ +@Composable +internal fun DecorateNavEntry( + entry: NavEntry, + entryDecorators: List<@JvmSuppressWildcards NavEntryDecorator<*>> +) { + @Suppress("UNCHECKED_CAST") + (entryDecorators as List<@JvmSuppressWildcards NavEntryDecorator>) + .distinct() + .foldRight(initial = entry) { decorator, wrappedEntry -> + object : NavEntryWrapper(wrappedEntry) { + override val content: @Composable ((T) -> Unit) = { + decorator.navEntryDecorator(wrappedEntry) + } + } + } + .content + .invoke(entry.key) +} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/MovableContentNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/MovableContentNavEntryDecorator.kt index 5be9c83..4b6cef2 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/MovableContentNavEntryDecorator.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/MovableContentNavEntryDecorator.kt @@ -17,40 +17,40 @@ package com.tunjid.treenav.compose.navigation3.decorators import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.key import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf -import com.tunjid.treenav.compose.navigation3.NavEntry import com.tunjid.treenav.compose.navigation3.NavEntryDecorator +import com.tunjid.treenav.compose.navigation3.navEntryDecorator +/** Returns a [MovableContentNavEntryDecorator] that is remembered across recompositions. */ +@Composable +internal fun rememberMovableContentNavEntryDecorator(): NavEntryDecorator = remember { + MovableContentNavEntryDecorator() +} /** * A [NavEntryDecorator] that wraps each entry in a [movableContentOf] to allow nav displays to - * arbitrarily place entries in different places in the composable call hierarchy. + * arbitrarily place entries in different places in the composable call hierarchy and ensures that + * the same entry content is not composed multiple times in different places of the hierarchy. * - * This should likely be the first [NavEntryDecorator] to ensure that other - * [NavEntryDecorator.DecorateEntry] calls that are stateful are moved properly inside the - * [movableContentOf]. + * This should likely be the first [NavEntryDecorator] to ensure that other [NavEntryDecorator] + * calls that are stateful are moved properly inside the [movableContentOf]. */ -internal object MovableContentNavEntryDecorator : NavEntryDecorator { - - @Composable - override fun DecorateBackStack(backStack: List, content: @Composable (() -> Unit)) { - val backStackKeys = backStack.toSet() - - // This is an intricate dance to create a movableContentOf for each entry that is scoped - // to the backstack, that calls the correct updated content. - // First we associate each key in the backstack with a MutableState that will contain - // the actual content of the entry, as updated in DecorateEntry. - // The MutableState's remembered lifecycle precisely matches when its key is in the - // backstack. - val movableContentContentHolderMap: Map Unit>> = - backStackKeys.associateWith { key -> +private fun MovableContentNavEntryDecorator(): NavEntryDecorator { + val movableContentContentHolderMap: MutableMap Unit>> = + mutableMapOf() + val movableContentHolderMap: MutableMap Unit> = mutableMapOf() + return navEntryDecorator( + onPop = { + movableContentHolderMap.remove(it) + movableContentContentHolderMap.remove(it) + }, + decorator = { entry -> + val key = entry.key + movableContentContentHolderMap.getOrPut(key) { key(key) { remember { mutableStateOf( @@ -64,13 +64,7 @@ internal object MovableContentNavEntryDecorator : NavEntryDecorator { } } } - - // Second we create another map containing the movable contents themselves, again - // by associating the backstack key with a remembered movableContentOf - // The critical thing here is that the movableContentOf's remembered lifecycle precisely - // matches when its key is in the backstack. - val movableContentHolderMap: Map Unit> = - backStackKeys.associateWith { key -> + movableContentHolderMap.getOrPut(key) { key(key) { remember { movableContentOf { @@ -82,47 +76,23 @@ internal object MovableContentNavEntryDecorator : NavEntryDecorator { } } } - CompositionLocalProvider( - LocalMovableContentNavLocalInfo provides - MovableContentNavLocalInfo(movableContentHolderMap, movableContentContentHolderMap), - content = content, - ) - } - @Composable - override fun DecorateEntry(entry: NavEntry) { - val movableContentNavLocalInfo = LocalMovableContentNavLocalInfo.current - key(entry.key) { - // In case the key is removed from the backstack while this is still - // being rendered, we remember the MutableState directly to allow - // updating it while we are animating out. - val movableContentContentHolder = remember { - movableContentNavLocalInfo.movableContentContentHolderMap.getValue(entry.key) - } - // Update the state holder with the actual entry content - movableContentContentHolder.value = { entry.content(entry.key) } - // In case the key is removed from the backstack while this is still - // being rendered, we remember the movableContent directly to allow - // rendering it while we are animating out. - val movableContentHolder = remember { - movableContentNavLocalInfo.movableContentHolderMap.getValue(entry.key) + key(key) { + // In case the key is removed from the backstack while this is still + // being rendered, we remember the MutableState directly to allow + // updating it while we are animating out. + val movableContentContentHolder = remember { + movableContentContentHolderMap.getValue(key) + } + // Update the state holder with the actual entry content + movableContentContentHolder.value = { entry.content(key) } + // In case the key is removed from the backstack while this is still + // being rendered, we remember the movableContent directly to allow + // rendering it while we are animating out. + val movableContentHolder = remember { movableContentHolderMap.getValue(key) } + // Finally, render the entry content via the movableContentOf + movableContentHolder() } - // Finally, render the entry content via the movableContentOf - movableContentHolder() } - } + ) } - -internal val LocalMovableContentNavLocalInfo = - staticCompositionLocalOf { - error( - "CompositionLocal LocalMovableContentNavLocalInfo not present. You must call " + - "DecorateBackStack before calling DecorateEntry." - ) - } - -@Immutable -internal class MovableContentNavLocalInfo( - val movableContentHolderMap: Map Unit>, - val movableContentContentHolderMap: Map Unit>> -) 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 deleted file mode 100644 index 59fd04b..0000000 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/SaveableStateNavEntryDecorator.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 - -/** - * 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 index 1d93398..e1bb023 100644 --- 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 @@ -18,11 +18,13 @@ 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.remember +import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleRegistry import androidx.savedstate.SavedState @@ -33,18 +35,52 @@ import androidx.savedstate.compose.LocalSavedStateRegistryOwner import androidx.savedstate.savedState import com.tunjid.treenav.compose.navigation3.NavEntry import com.tunjid.treenav.compose.navigation3.NavEntryDecorator +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. + * Returns a [SavedStateNavEntryDecorator] that is remembered across recompositions. + * + * @param saveableStateHolder the [SaveableStateHolder] that scopes the returned NavEntryDecorator */ -internal object SavedStateNavEntryDecorator : NavEntryDecorator { +@Composable +internal fun rememberSavedStateNavEntryDecorator( + saveableStateHolder: SaveableStateHolder = rememberSaveableStateHolder() +): NavEntryDecorator = remember { SavedStateNavEntryDecorator(saveableStateHolder) } - @Composable - override fun DecorateEntry(entry: NavEntry) { +/** + * 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. + * Also provides the content of a [NavEntry] with a [SavedStateRegistryOwner] which can be accessed + * in the content with [LocalSavedStateRegistryOwner]. + * + * This [NavEntryDecorator] is the only one that is **required** as saving state is considered a + * non-optional feature. + */ +private fun SavedStateNavEntryDecorator( + saveableStateHolder: SaveableStateHolder +): NavEntryDecorator { + val registryMap = mutableMapOf() + + val onPop: (Any) -> Unit = { key -> + val id = getIdForKey(key) + if (registryMap.contains(id)) { + // saveableStateHolder onPop + saveableStateHolder.removeState(id) + + // saved state onPop + val savedState = savedState() + val childRegistry = registryMap.getValue(id) + childRegistry.savedStateRegistryController.performSave(savedState) + childRegistry.savedState = savedState + childRegistry.lifecycle.currentState = Lifecycle.State.DESTROYED + } + } + + return navEntryDecorator(onPop = onPop) { entry -> val key = entry.key + val id = getIdForKey(key) + val childRegistry by rememberSaveable( key, @@ -56,26 +92,23 @@ internal object SavedStateNavEntryDecorator : NavEntryDecorator { ) { mutableStateOf(EntrySavedStateRegistry()) } + registryMap.put(id, childRegistry) - 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 + saveableStateHolder.SaveableStateProvider(id) { + CompositionLocalProvider(LocalSavedStateRegistryOwner provides childRegistry) { + entry.content(key) } } + childRegistry.lifecycle.currentState = Lifecycle.State.RESUMED } } +private fun getIdForKey(key: Any): String = "${key::class.qualifiedName}:$key" + private class EntrySavedStateRegistry : SavedStateRegistryOwner { override val lifecycle: LifecycleRegistry = LifecycleRegistry(this) - val savedStateRegistryController = SavedStateRegistryController.create(this) + val savedStateRegistryController: SavedStateRegistryController = + SavedStateRegistryController.create(this) override val savedStateRegistry: SavedStateRegistry = savedStateRegistryController.savedStateRegistry diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt index d505400..d997409 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt @@ -16,39 +16,22 @@ 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.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.LocalLifecycleOwner -import com.tunjid.treenav.compose.navigation3.NavEntry -import com.tunjid.treenav.compose.navigation3.NavEntryDecorator - -internal class TransitionAwareLifecycleNavEntryDecorator : NavEntryDecorator { - - var isSettled by mutableStateOf(true) - - @Composable - override fun DecorateBackStack(backStack: List, content: @Composable (() -> Unit)) { - val localInfo = remember(backStack) { TransitionAwareLifecycleNavLocalInfo(backStack) } - CompositionLocalProvider(LocalTransitionAwareLifecycleNavLocalInfo provides localInfo) { - content.invoke() - } - } +import com.tunjid.treenav.compose.navigation3.navEntryDecorator - @Composable - override fun DecorateEntry(entry: NavEntry) { - val backStack = LocalTransitionAwareLifecycleNavLocalInfo.current.backStack - // TODO: Handle duplicate keys +@Composable +internal fun transitionAwareLifecycleNavEntryDecorator(backStack: List, isSettled: Boolean) = + navEntryDecorator { entry -> val isInBackStack = entry.key in backStack val maxLifecycle = when { @@ -58,23 +41,12 @@ internal class TransitionAwareLifecycleNavEntryDecorator : NavEntryDecorator { } LifecycleOwner(maxLifecycle = maxLifecycle) { entry.content.invoke(entry.key) } } -} - -private val LocalTransitionAwareLifecycleNavLocalInfo = - compositionLocalOf { - error( - "CompositionLocal LocalTransitionAwareLifecycleNavLocalInfo not present. You must " + - "call DecorateBackStack before calling DecorateEntry." - ) - } - -private class TransitionAwareLifecycleNavLocalInfo(val backStack: List) @Composable private fun LifecycleOwner( maxLifecycle: Lifecycle.State = Lifecycle.State.RESUMED, parentLifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - content: @Composable () -> Unit, + content: @Composable () -> Unit ) { val childLifecycleOwner = remember(parentLifecycleOwner) { ChildLifecycleOwner() } // Pass LifecycleEvents from the parent down to the child diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.kt index adc68a3..2bdd320 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.kt @@ -18,11 +18,7 @@ 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 import androidx.lifecycle.HasDefaultViewModelProviderFactory import androidx.lifecycle.Lifecycle import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY @@ -36,127 +32,67 @@ 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.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.compose.LocalSavedStateRegistryOwner -import com.tunjid.treenav.compose.navigation3.NavEntry import com.tunjid.treenav.compose.navigation3.NavEntryDecorator +import com.tunjid.treenav.compose.navigation3.navEntryDecorator -@Stable -internal expect val ViewModelStoreNavEntryDecorator: NavEntryDecorator +@Composable internal expect fun shouldRemoveViewModelStoreCallback(): () -> Boolean + +internal expect fun SavedStateViewModelFactory( + savedStateRegistryOwner: SavedStateRegistryOwner +): SavedStateViewModelFactory + +/** + * Returns a [ViewModelStoreNavEntryDecorator] that is remembered across recompositions. + * + * @param [viewModelStoreOwner] The [ViewModelStoreOwner] that provides the [ViewModelStore] to + * NavEntries + * @param [shouldRemoveStoreOwner] A lambda that returns a Boolean for whether the store owner for a + * [NavEntry] should be cleared when the [NavEntry] is popped from the backStack. If true, the + * entry's ViewModelStoreOwner will be removed. + */ +@Composable +internal fun rememberViewModelStoreNavEntryDecorator( + viewModelStoreOwner: ViewModelStoreOwner = + checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + shouldRemoveStoreOwner: () -> Boolean = shouldRemoveViewModelStoreCallback() +): NavEntryDecorator = remember { + ViewModelStoreNavEntryDecorator(viewModelStoreOwner.viewModelStore, shouldRemoveStoreOwner) +} /** * 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 + * This requires the usage of [androidx.navigation3.runtime.SavedStateNavEntryDecorator] to ensure + * that the [NavEntry] scoped [ViewModel]s can properly provide access to + * [androidx.lifecycle.SavedStateHandle]s + * + * @param [viewModelStore] The [ViewModelStore] that provides to NavEntries + * @param [shouldRemoveStoreOwner] A lambda that returns a Boolean for whether the store owner for a + * [NavEntry] should be cleared when the [NavEntry] is popped from the backStack. If true, the + * entry's ViewModelStoreOwner will be removed. */ -internal object DefaultViewModelStoreNavEntryDecorator : 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() +internal fun ViewModelStoreNavEntryDecorator( + viewModelStore: ViewModelStore, + shouldRemoveStoreOwner: () -> Boolean, +): NavEntryDecorator { + val storeOwnerProvider: EntryViewModel = viewModelStore.getEntryViewModel() + val onPop: (Any) -> Unit = { key -> + if (shouldRemoveStoreOwner()) { + storeOwnerProvider.clearViewModelStoreOwnerForKey(key) } } - - @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) - } - } - } - } + return navEntryDecorator(onPop) { entry -> + val viewModelStore = storeOwnerProvider.viewModelStoreForKey(entry.key) val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current - val childViewModelOwner = remember { + val childViewModelStoreOwner = remember { object : ViewModelStoreOwner, SavedStateRegistryOwner by savedStateRegistryOwner, @@ -165,7 +101,7 @@ internal object DefaultViewModelStoreNavEntryDecorator : NavEntryDecorator { get() = viewModelStore override val defaultViewModelProviderFactory: ViewModelProvider.Factory - get() = SavedStateViewModelFactory() + get() = SavedStateViewModelFactory(savedStateRegistryOwner) override val defaultViewModelCreationExtras: CreationExtras get() = @@ -185,35 +121,31 @@ internal object DefaultViewModelStoreNavEntryDecorator : NavEntryDecorator { } } } - CompositionLocalProvider(LocalViewModelStoreOwner provides childViewModelOwner) { - entry.content.invoke(key) + CompositionLocalProvider(LocalViewModelStoreOwner provides childViewModelStoreOwner) { + entry.content.invoke(entry.key) } } } -internal class EntryViewModel : ViewModel() { +private class EntryViewModel : ViewModel() { private val owners = mutableMapOf() - val ownerInBackStack = mutableListOf() fun viewModelStoreForKey(key: Any): ViewModelStore = owners.getOrPut(key) { ViewModelStore() } - fun removeViewModelStoreOwnerForKey(key: Any): ViewModelStore? = owners.remove(key) + fun clearViewModelStoreOwnerForKey(key: Any) { + owners.remove(key)?.clear() + } override fun onCleared() { owners.forEach { (_, store) -> store.clear() } } } -internal val LocalViewModelStoreNavLocalInfo = - staticCompositionLocalOf { - error( - "CompositionLocal LocalViewModelStoreNavLocalInfo not present. You must call " + - "DecorateBackStack before calling DecorateEntry." +private fun ViewModelStore.getEntryViewModel(): EntryViewModel { + val provider = + ViewModelProvider.create( + store = this, + factory = viewModelFactory { initializer { EntryViewModel() } }, ) - } - -internal class ViewModelStoreNavLocalInfo { - internal val refCount: MutableMap> = mutableMapOf() - @Suppress("PrimitiveInCollection") // The order of the element matters - internal val idsInComposition: LinkedHashSet = LinkedHashSet() -} + return provider[EntryViewModel::class] +} \ No newline at end of file diff --git a/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.kt b/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.kt index adbfd92..4346b76 100644 --- a/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.kt +++ b/library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.kt @@ -16,9 +16,15 @@ package com.tunjid.treenav.compose.navigation3.decorators -import androidx.compose.runtime.Stable -import com.tunjid.treenav.compose.navigation3.NavEntryDecorator +import androidx.compose.runtime.Composable +import androidx.lifecycle.SavedStateViewModelFactory +import androidx.savedstate.SavedStateRegistryOwner -@Stable -internal actual val ViewModelStoreNavEntryDecorator: NavEntryDecorator - get() = DefaultViewModelStoreNavEntryDecorator \ No newline at end of file +@Composable +internal actual fun shouldRemoveViewModelStoreCallback(): () -> Boolean { + return { true } +} + +internal actual fun SavedStateViewModelFactory( + savedStateRegistryOwner: SavedStateRegistryOwner +): SavedStateViewModelFactory = SavedStateViewModelFactory() \ No newline at end of file diff --git a/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.kt b/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.kt index adbfd92..4346b76 100644 --- a/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.kt +++ b/library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.kt @@ -16,9 +16,15 @@ package com.tunjid.treenav.compose.navigation3.decorators -import androidx.compose.runtime.Stable -import com.tunjid.treenav.compose.navigation3.NavEntryDecorator +import androidx.compose.runtime.Composable +import androidx.lifecycle.SavedStateViewModelFactory +import androidx.savedstate.SavedStateRegistryOwner -@Stable -internal actual val ViewModelStoreNavEntryDecorator: NavEntryDecorator - get() = DefaultViewModelStoreNavEntryDecorator \ No newline at end of file +@Composable +internal actual fun shouldRemoveViewModelStoreCallback(): () -> Boolean { + return { true } +} + +internal actual fun SavedStateViewModelFactory( + savedStateRegistryOwner: SavedStateRegistryOwner +): SavedStateViewModelFactory = SavedStateViewModelFactory() \ No newline at end of file From 6e78c5c1d0eca00a606afc1ea707ac37d89ade6e Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 18:07:46 -0400 Subject: [PATCH 2/2] Rework TransitionAwareLifecycleNavEntryDecorator --- .../DecoratedNavEntryMultiPaneDisplayScope.kt | 11 ++++--- ...ansitionAwareLifecycleNavEntryDecorator.kt | 29 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) 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 6a53b5c..86d579e 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 @@ -60,9 +60,6 @@ internal fun DecoratedNavEntr state.destinationTransform(navigationState) ) - val transitionAwareLifecycleNavEntryDecorator = - transitionAwareLifecycleNavEntryDecorator(backStack, true) - DecoratedNavEntryProvider( backStack = backStack, entryProvider = { node -> @@ -78,7 +75,13 @@ internal fun DecoratedNavEntr entryDecorators = listOf( rememberMovableContentNavEntryDecorator(), rememberSavedStateNavEntryDecorator(), - transitionAwareLifecycleNavEntryDecorator, + transitionAwareLifecycleNavEntryDecorator( + backStack = backStack, + isSettled = { + val scope = LocalPaneScope.current + scope.transition.currentState == scope.transition.targetState + } + ), rememberViewModelStoreNavEntryDecorator(), ), content = { entries -> diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt index d997409..6f6c0b5 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.kt @@ -30,17 +30,20 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import com.tunjid.treenav.compose.navigation3.navEntryDecorator @Composable -internal fun transitionAwareLifecycleNavEntryDecorator(backStack: List, isSettled: Boolean) = - navEntryDecorator { entry -> - val isInBackStack = entry.key in backStack - val maxLifecycle = - when { - isInBackStack && isSettled -> Lifecycle.State.RESUMED - isInBackStack && !isSettled -> Lifecycle.State.STARTED - else /* !isInBackStack */ -> Lifecycle.State.CREATED - } - LifecycleOwner(maxLifecycle = maxLifecycle) { entry.content.invoke(entry.key) } - } +internal fun transitionAwareLifecycleNavEntryDecorator( + backStack: List, + isSettled: @Composable () -> Boolean +) = navEntryDecorator { entry -> + val isInBackStack = entry.key in backStack + val settled = isSettled() + val maxLifecycle = + when { + isInBackStack && settled -> Lifecycle.State.RESUMED + isInBackStack && !settled -> Lifecycle.State.STARTED + else /* !isInBackStack */ -> Lifecycle.State.CREATED + } + LifecycleOwner(maxLifecycle = maxLifecycle) { entry.content.invoke(entry.key) } +} @Composable private fun LifecycleOwner( @@ -64,7 +67,9 @@ private fun LifecycleOwner( childLifecycleOwner.maxLifecycle = maxLifecycle } // Now install the LifecycleOwner as a composition local - CompositionLocalProvider(LocalLifecycleOwner provides childLifecycleOwner) { content.invoke() } + CompositionLocalProvider(LocalLifecycleOwner provides childLifecycleOwner) { + content.invoke() + } } private class ChildLifecycleOwner : LifecycleOwner {