| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package androidx.navigation.fragment |
| |
| import android.content.Context |
| import android.os.Bundle |
| import android.util.AttributeSet |
| import android.util.Log |
| import android.view.View |
| import androidx.annotation.CallSuper |
| import androidx.core.content.res.use |
| import androidx.core.os.bundleOf |
| import androidx.fragment.app.Fragment |
| import androidx.fragment.app.FragmentManager |
| import androidx.fragment.app.FragmentManager.OnBackStackChangedListener |
| import androidx.fragment.app.FragmentTransaction |
| import androidx.lifecycle.Lifecycle |
| import androidx.lifecycle.LifecycleEventObserver |
| import androidx.lifecycle.ViewModel |
| import androidx.lifecycle.ViewModelProvider |
| import androidx.lifecycle.viewmodel.CreationExtras |
| import androidx.lifecycle.viewmodel.initializer |
| import androidx.lifecycle.viewmodel.viewModelFactory |
| import androidx.navigation.NavBackStackEntry |
| import androidx.navigation.NavController |
| import androidx.navigation.NavDestination |
| import androidx.navigation.NavOptions |
| import androidx.navigation.Navigator |
| import androidx.navigation.NavigatorProvider |
| import androidx.navigation.NavigatorState |
| import androidx.navigation.fragment.FragmentNavigator.Destination |
| import java.lang.ref.WeakReference |
| |
| /** |
| * Navigator that navigates through [fragment transactions][FragmentTransaction]. Every |
| * destination using this Navigator must set a valid Fragment class name with |
| * `android:name` or [Destination.setClassName]. |
| * |
| * The current Fragment from FragmentNavigator's perspective can be retrieved by calling |
| * [FragmentManager.getPrimaryNavigationFragment] with the FragmentManager |
| * passed to this FragmentNavigator. |
| * |
| * Note that the default implementation does Fragment transactions |
| * asynchronously, so the current Fragment will not be available immediately |
| * (i.e., in callbacks to [NavController.OnDestinationChangedListener]). |
| */ |
| @Navigator.Name("fragment") |
| public open class FragmentNavigator( |
| private val context: Context, |
| private val fragmentManager: FragmentManager, |
| private val containerId: Int |
| ) : Navigator<Destination>() { |
| private val savedIds = mutableSetOf<String>() |
| |
| /** |
| * A list of pending operations within a Transaction expected to be executed by FragmentManager. |
| * Pending ops are added at the start of a transaction, and by the time a transaction completes, |
| * this list is expected to be cleared. |
| * |
| * In general, each entry would be added only once to this list within a single transaction |
| * except in the case of singleTop transactions. Single top transactions involve two |
| * fragment instances with the same entry, so we would get two onBackStackChanged callbacks |
| * on the same entry. |
| * |
| * Each Pair represents the entry.id and whether this entry is getting popped |
| */ |
| internal val pendingOps = mutableListOf<Pair<String, Boolean>>() |
| |
| /** |
| * Get the back stack from the [state]. |
| */ |
| internal val backStack get() = state.backStack |
| |
| private val fragmentObserver = LifecycleEventObserver { source, event -> |
| if (event == Lifecycle.Event.ON_DESTROY) { |
| val fragment = source as Fragment |
| val entry = state.transitionsInProgress.value.lastOrNull { entry -> |
| entry.id == fragment.tag |
| } |
| if (entry != null) { |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "Marking transition complete for entry $entry " + |
| "due to fragment $source lifecycle reaching DESTROYED" |
| ) |
| } |
| state.markTransitionComplete(entry) |
| } |
| } |
| } |
| |
| private val fragmentViewObserver = { entry: NavBackStackEntry -> |
| LifecycleEventObserver { owner, event -> |
| // Once the lifecycle reaches RESUMED, if the entry is in the back stack we can mark |
| // the transition complete |
| if (event == Lifecycle.Event.ON_RESUME && state.backStack.value.contains(entry)) { |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "Marking transition complete for entry $entry due " + |
| "to fragment $owner view lifecycle reaching RESUMED" |
| ) |
| } |
| state.markTransitionComplete(entry) |
| } |
| // Once the lifecycle reaches DESTROYED, we can mark the transition complete |
| if (event == Lifecycle.Event.ON_DESTROY) { |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "Marking transition complete for entry $entry due " + |
| "to fragment $owner view lifecycle reaching DESTROYED" |
| ) |
| } |
| state.markTransitionComplete(entry) |
| } |
| } |
| } |
| |
| override fun onAttach(state: NavigatorState) { |
| super.onAttach(state) |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v(TAG, "onAttach") |
| } |
| |
| fragmentManager.addFragmentOnAttachListener { _, fragment -> |
| val entry = state.backStack.value.lastOrNull { it.id == fragment.tag } |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "Attaching fragment $fragment associated with entry " + |
| "$entry to FragmentManager $fragmentManager" |
| ) |
| } |
| if (entry != null) { |
| attachObservers(entry, fragment) |
| // We need to ensure that if the fragment has its state saved and then that state |
| // later cleared without the restoring the fragment that we also clear the state |
| // of the associated entry. |
| attachClearViewModel(fragment, entry, state) |
| } |
| } |
| |
| fragmentManager.addOnBackStackChangedListener(object : OnBackStackChangedListener { |
| override fun onBackStackChanged() { } |
| |
| override fun onBackStackChangeStarted(fragment: Fragment, pop: Boolean) { |
| // We only care about the pop case here since in the navigate case by the time |
| // we get here the fragment will have already been moved to STARTED. |
| // In the case of a pop, we move the entries to STARTED |
| if (pop) { |
| val entry = state.backStack.value.lastOrNull { it.id == fragment.tag } |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "OnBackStackChangedStarted for fragment " + |
| "$fragment associated with entry $entry" |
| ) |
| } |
| entry?.let { state.prepareForTransition(it) } |
| } |
| } |
| |
| override fun onBackStackChangeCommitted(fragment: Fragment, pop: Boolean) { |
| val entry = (state.backStack.value + state.transitionsInProgress.value).lastOrNull { |
| it.id == fragment.tag |
| } |
| |
| // In case of system back, all pending transactions are executed before handling |
| // back press, hence pendingOps will be empty. |
| val isSystemBack = pop && pendingOps.isEmpty() && fragment.isRemoving |
| val op = pendingOps.firstOrNull { it.first == fragment.tag } |
| op?.let { pendingOps.remove(it) } |
| |
| if (!isSystemBack && FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "OnBackStackChangedCommitted for fragment " + |
| "$fragment associated with entry $entry" |
| ) |
| } |
| |
| val popOp = op?.second == true |
| if (!pop && !popOp) { |
| requireNotNull(entry) { |
| "The fragment " + fragment + " is unknown to the FragmentNavigator. " + |
| "Please use the navigate() function to add fragments to the " + |
| "FragmentNavigator managed FragmentManager." |
| } |
| } |
| if (entry != null) { |
| // In case we get a fragment that was never attached to the fragment manager, |
| // we need to make sure we still return the entries to their proper final state. |
| attachClearViewModel(fragment, entry, state) |
| // This is the case of system back where we will need to make the call to |
| // popBackStack. Otherwise, popBackStack was called directly and we avoid |
| // popping again. |
| if (isSystemBack) { |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "OnBackStackChangedCommitted for fragment $fragment " + |
| "popping associated entry $entry via system back" |
| ) |
| } |
| state.popWithTransition(entry, false) |
| } |
| } |
| } |
| }) |
| } |
| |
| private fun attachObservers(entry: NavBackStackEntry, fragment: Fragment) { |
| fragment.viewLifecycleOwnerLiveData.observe(fragment) { owner -> |
| // attach observer unless it was already popped at this point |
| // we get onBackStackStackChangedCommitted callback for an executed navigate where we |
| // remove incoming fragment from pendingOps before ATTACH so the listener will still |
| // be added |
| val isPending = pendingOps.any { it.first == fragment.tag } |
| if (owner != null && !isPending) { |
| val viewLifecycle = fragment.viewLifecycleOwner.lifecycle |
| // We only need to add observers while the viewLifecycle has not reached a final |
| // state |
| if (viewLifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) { |
| viewLifecycle.addObserver(fragmentViewObserver(entry)) |
| } |
| } |
| } |
| fragment.lifecycle.addObserver(fragmentObserver) |
| } |
| |
| internal fun attachClearViewModel( |
| fragment: Fragment, |
| entry: NavBackStackEntry, |
| state: NavigatorState |
| ) { |
| val viewModel = ViewModelProvider( |
| fragment.viewModelStore, |
| viewModelFactory { initializer { ClearEntryStateViewModel() } }, |
| CreationExtras.Empty |
| )[ClearEntryStateViewModel::class.java] |
| viewModel.completeTransition = |
| WeakReference { |
| entry.let { |
| state.transitionsInProgress.value.forEach { entry -> |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "Marking transition complete for entry " + |
| "$entry due to fragment $fragment viewmodel being cleared" |
| ) |
| } |
| state.markTransitionComplete(entry) |
| } |
| } |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * This method must call |
| * [FragmentTransaction.setPrimaryNavigationFragment] |
| * if the pop succeeded so that the newly visible Fragment can be retrieved with |
| * [FragmentManager.getPrimaryNavigationFragment]. |
| * |
| * Note that the default implementation pops the Fragment |
| * asynchronously, so the newly visible Fragment from the back stack |
| * is not instantly available after this call completes. |
| */ |
| override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { |
| if (fragmentManager.isStateSaved) { |
| Log.i( |
| TAG, "Ignoring popBackStack() call: FragmentManager has already saved its state" |
| ) |
| return |
| } |
| val beforePopList = state.backStack.value |
| // Get the set of entries that are going to be popped |
| val popUpToIndex = beforePopList.indexOf(popUpTo) |
| val poppedList = beforePopList.subList( |
| popUpToIndex, |
| beforePopList.size |
| ) |
| val initialEntry = beforePopList.first() |
| if (savedState) { |
| // Now go through the list in reversed order (i.e., started from the most added) |
| // and save the back stack state of each. |
| for (entry in poppedList.reversed()) { |
| if (entry == initialEntry) { |
| Log.i( |
| TAG, |
| "FragmentManager cannot save the state of the initial destination $entry" |
| ) |
| } else { |
| fragmentManager.saveBackStack(entry.id) |
| savedIds += entry.id |
| } |
| } |
| } else { |
| fragmentManager.popBackStack( |
| popUpTo.id, |
| FragmentManager.POP_BACK_STACK_INCLUSIVE |
| ) |
| } |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "Calling popWithTransition via popBackStack() on entry " + |
| "$popUpTo with savedState $savedState" |
| ) |
| } |
| |
| val incomingEntry = beforePopList.elementAtOrNull(popUpToIndex - 1) |
| if (incomingEntry != null) { |
| addPendingOps(incomingEntry.id) |
| } |
| // add pending ops here before any animation (if present) starts |
| poppedList.filter { it.id != initialEntry.id }.forEach { entry -> |
| addPendingOps(entry.id, isPop = true) |
| } |
| |
| state.popWithTransition(popUpTo, savedState) |
| } |
| |
| public override fun createDestination(): Destination { |
| return Destination(this) |
| } |
| |
| /** |
| * Instantiates the Fragment via the FragmentManager's |
| * [androidx.fragment.app.FragmentFactory]. |
| * |
| * Note that this method is **not** responsible for calling |
| * [Fragment.setArguments] on the returned Fragment instance. |
| * |
| * @param context Context providing the correct [ClassLoader] |
| * @param fragmentManager FragmentManager the Fragment will be added to |
| * @param className The Fragment to instantiate |
| * @param args The Fragment's arguments, if any |
| * @return A new fragment instance. |
| */ |
| @Suppress("DeprecatedCallableAddReplaceWith") |
| @Deprecated( |
| """Set a custom {@link androidx.fragment.app.FragmentFactory} via |
| {@link FragmentManager#setFragmentFactory(FragmentFactory)} to control |
| instantiation of Fragments.""" |
| ) |
| public open fun instantiateFragment( |
| context: Context, |
| fragmentManager: FragmentManager, |
| className: String, |
| args: Bundle? |
| ): Fragment { |
| return fragmentManager.fragmentFactory.instantiate(context.classLoader, className) |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * This method should always call |
| * [FragmentTransaction.setPrimaryNavigationFragment] |
| * so that the Fragment associated with the new destination can be retrieved with |
| * [FragmentManager.getPrimaryNavigationFragment]. |
| * |
| * Note that the default implementation commits the new Fragment |
| * asynchronously, so the new Fragment is not instantly available |
| * after this call completes. |
| * |
| * This call will be ignored if the FragmentManager state has already been saved. |
| */ |
| override fun navigate( |
| entries: List<NavBackStackEntry>, |
| navOptions: NavOptions?, |
| navigatorExtras: Navigator.Extras? |
| ) { |
| if (fragmentManager.isStateSaved) { |
| Log.i( |
| TAG, "Ignoring navigate() call: FragmentManager has already saved its state" |
| ) |
| return |
| } |
| for (entry in entries) { |
| navigate(entry, navOptions, navigatorExtras) |
| } |
| } |
| |
| private fun navigate( |
| entry: NavBackStackEntry, |
| navOptions: NavOptions?, |
| navigatorExtras: Navigator.Extras? |
| ) { |
| val initialNavigation = state.backStack.value.isEmpty() |
| val restoreState = ( |
| navOptions != null && !initialNavigation && |
| navOptions.shouldRestoreState() && |
| savedIds.remove(entry.id) |
| ) |
| if (restoreState) { |
| // Restore back stack does all the work to restore the entry |
| fragmentManager.restoreBackStack(entry.id) |
| state.pushWithTransition(entry) |
| return |
| } |
| val ft = createFragmentTransaction(entry, navOptions) |
| |
| if (!initialNavigation) { |
| val outgoingEntry = state.backStack.value.lastOrNull() |
| // if outgoing entry is initial entry, FragmentManager still triggers onBackStackChange |
| // callback for it, so we don't filter out initial entry here |
| if (outgoingEntry != null) { |
| addPendingOps(outgoingEntry.id) |
| } |
| // add pending ops here before any animation (if present) starts |
| addPendingOps(entry.id) |
| ft.addToBackStack(entry.id) |
| } |
| |
| if (navigatorExtras is Extras) { |
| for ((key, value) in navigatorExtras.sharedElements) { |
| ft.addSharedElement(key, value) |
| } |
| } |
| ft.commit() |
| // The commit succeeded, update our view of the world |
| if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { |
| Log.v( |
| TAG, |
| "Calling pushWithTransition via navigate() on entry $entry" |
| ) |
| } |
| state.pushWithTransition(entry) |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * This method should always call |
| * [FragmentTransaction.setPrimaryNavigationFragment] |
| * so that the Fragment associated with the new destination can be retrieved with |
| * [FragmentManager.getPrimaryNavigationFragment]. |
| * |
| * Note that the default implementation commits the new Fragment |
| * asynchronously, so the new Fragment is not instantly available |
| * after this call completes. |
| * |
| * This call will be ignored if the FragmentManager state has already been saved. |
| */ |
| override fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) { |
| if (fragmentManager.isStateSaved) { |
| Log.i( |
| TAG, |
| "Ignoring onLaunchSingleTop() call: FragmentManager has already saved its state" |
| ) |
| return |
| } |
| val ft = createFragmentTransaction(backStackEntry, null) |
| val backstack = state.backStack.value |
| if (backstack.size > 1) { |
| // If the Fragment to be replaced is on the FragmentManager's |
| // back stack, a simple replace() isn't enough so we |
| // remove it from the back stack and put our replacement |
| // on the back stack in its place |
| val incomingEntry = backstack.elementAtOrNull(backstack.lastIndex - 1) |
| if (incomingEntry != null) { |
| addPendingOps(incomingEntry.id) |
| } |
| addPendingOps(backStackEntry.id, isPop = true) |
| fragmentManager.popBackStack( |
| backStackEntry.id, |
| FragmentManager.POP_BACK_STACK_INCLUSIVE |
| ) |
| |
| addPendingOps(backStackEntry.id, deduplicate = false) |
| ft.addToBackStack(backStackEntry.id) |
| } |
| ft.commit() |
| // The commit succeeded, update our view of the world |
| state.onLaunchSingleTop(backStackEntry) |
| } |
| |
| private fun createFragmentTransaction( |
| entry: NavBackStackEntry, |
| navOptions: NavOptions? |
| ): FragmentTransaction { |
| val destination = entry.destination as Destination |
| val args = entry.arguments |
| var className = destination.className |
| if (className[0] == '.') { |
| className = context.packageName + className |
| } |
| val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className) |
| frag.arguments = args |
| val ft = fragmentManager.beginTransaction() |
| var enterAnim = navOptions?.enterAnim ?: -1 |
| var exitAnim = navOptions?.exitAnim ?: -1 |
| var popEnterAnim = navOptions?.popEnterAnim ?: -1 |
| var popExitAnim = navOptions?.popExitAnim ?: -1 |
| if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) { |
| enterAnim = if (enterAnim != -1) enterAnim else 0 |
| exitAnim = if (exitAnim != -1) exitAnim else 0 |
| popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0 |
| popExitAnim = if (popExitAnim != -1) popExitAnim else 0 |
| ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) |
| } |
| ft.replace(containerId, frag, entry.id) |
| ft.setPrimaryNavigationFragment(frag) |
| ft.setReorderingAllowed(true) |
| return ft |
| } |
| |
| public override fun onSaveState(): Bundle? { |
| if (savedIds.isEmpty()) { |
| return null |
| } |
| return bundleOf(KEY_SAVED_IDS to ArrayList(savedIds)) |
| } |
| |
| public override fun onRestoreState(savedState: Bundle) { |
| val savedIds = savedState.getStringArrayList(KEY_SAVED_IDS) |
| if (savedIds != null) { |
| this.savedIds.clear() |
| this.savedIds += savedIds |
| } |
| } |
| |
| /** |
| * NavDestination specific to [FragmentNavigator] |
| * |
| * Construct a new fragment destination. This destination is not valid until you set the |
| * Fragment via [setClassName]. |
| * |
| * @param fragmentNavigator The [FragmentNavigator] which this destination will be associated |
| * with. Generally retrieved via a [NavController]'s [NavigatorProvider.getNavigator] method. |
| */ |
| @NavDestination.ClassType(Fragment::class) |
| public open class Destination |
| public constructor(fragmentNavigator: Navigator<out Destination>) : |
| NavDestination(fragmentNavigator) { |
| |
| /** |
| * Construct a new fragment destination. This destination is not valid until you set the |
| * Fragment via [setClassName]. |
| * |
| * @param navigatorProvider The [NavController] which this destination |
| * will be associated with. |
| */ |
| public constructor(navigatorProvider: NavigatorProvider) : |
| this(navigatorProvider.getNavigator(FragmentNavigator::class.java)) |
| |
| @CallSuper |
| public override fun onInflate(context: Context, attrs: AttributeSet) { |
| super.onInflate(context, attrs) |
| context.resources.obtainAttributes(attrs, R.styleable.FragmentNavigator).use { array -> |
| val className = array.getString(R.styleable.FragmentNavigator_android_name) |
| if (className != null) setClassName(className) |
| } |
| } |
| |
| /** |
| * Set the Fragment class name associated with this destination |
| * @param className The class name of the Fragment to show when you navigate to this |
| * destination |
| * @return this [Destination] |
| */ |
| public fun setClassName(className: String): Destination { |
| _className = className |
| return this |
| } |
| |
| private var _className: String? = null |
| /** |
| * The Fragment's class name associated with this destination |
| * |
| * @throws IllegalStateException when no Fragment class was set. |
| */ |
| public val className: String |
| get() { |
| checkNotNull(_className) { "Fragment class was not set" } |
| return _className as String |
| } |
| |
| public override fun toString(): String { |
| val sb = StringBuilder() |
| sb.append(super.toString()) |
| sb.append(" class=") |
| if (_className == null) { |
| sb.append("null") |
| } else { |
| sb.append(_className) |
| } |
| return sb.toString() |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (other == null || other !is Destination) return false |
| return super.equals(other) && _className == other._className |
| } |
| |
| override fun hashCode(): Int { |
| var result = super.hashCode() |
| result = 31 * result + _className.hashCode() |
| return result |
| } |
| } |
| |
| /** |
| * Extras that can be passed to FragmentNavigator to enable Fragment specific behavior |
| */ |
| public class Extras internal constructor(sharedElements: Map<View, String>) : |
| Navigator.Extras { |
| private val _sharedElements = LinkedHashMap<View, String>() |
| |
| /** |
| * The map of shared elements associated with these Extras. The returned map |
| * is an [unmodifiable][Map] copy of the underlying map and should be treated as immutable. |
| */ |
| public val sharedElements: Map<View, String> |
| get() = _sharedElements.toMap() |
| |
| /** |
| * Builder for constructing new [Extras] instances. The resulting instances are |
| * immutable. |
| */ |
| public class Builder { |
| private val _sharedElements = LinkedHashMap<View, String>() |
| |
| /** |
| * Adds multiple shared elements for mapping Views in the current Fragment to |
| * transitionNames in the Fragment being navigated to. |
| * |
| * @param sharedElements Shared element pairs to add |
| * @return this [Builder] |
| */ |
| public fun addSharedElements(sharedElements: Map<View, String>): Builder { |
| for ((view, name) in sharedElements) { |
| addSharedElement(view, name) |
| } |
| return this |
| } |
| |
| /** |
| * Maps the given View in the current Fragment to the given transition name in the |
| * Fragment being navigated to. |
| * |
| * @param sharedElement A View in the current Fragment to match with a View in the |
| * Fragment being navigated to. |
| * @param name The transitionName of the View in the Fragment being navigated to that |
| * should be matched to the shared element. |
| * @return this [Builder] |
| * @see FragmentTransaction.addSharedElement |
| */ |
| public fun addSharedElement(sharedElement: View, name: String): Builder { |
| _sharedElements[sharedElement] = name |
| return this |
| } |
| |
| /** |
| * Constructs the final [Extras] instance. |
| * |
| * @return An immutable [Extras] instance. |
| */ |
| public fun build(): Extras { |
| return Extras(_sharedElements) |
| } |
| } |
| |
| init { |
| _sharedElements.putAll(sharedElements) |
| } |
| } |
| |
| private companion object { |
| private const val TAG = "FragmentNavigator" |
| private const val KEY_SAVED_IDS = "androidx-nav-fragment:navigator:savedIds" |
| } |
| |
| internal class ClearEntryStateViewModel : ViewModel() { |
| lateinit var completeTransition: WeakReference<() -> Unit> |
| override fun onCleared() { |
| super.onCleared() |
| completeTransition.get()?.invoke() |
| } |
| } |
| |
| /** |
| * In general, each entry would only get one callback within a transaction except |
| * for single top transactions, where we would get two callbacks for the same entry. |
| */ |
| private fun addPendingOps(id: String, isPop: Boolean = false, deduplicate: Boolean = true) { |
| if (deduplicate) { |
| pendingOps.removeAll { it.first == id } |
| } |
| pendingOps.add(id to isPop) |
| } |
| } |