1 /*
<lambda>null2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.navigation.fragment
17 
18 import android.content.Context
19 import android.os.Bundle
20 import android.util.AttributeSet
21 import android.util.Log
22 import android.view.View
23 import androidx.annotation.CallSuper
24 import androidx.core.content.res.use
25 import androidx.core.os.bundleOf
26 import androidx.fragment.app.Fragment
27 import androidx.fragment.app.FragmentManager
28 import androidx.fragment.app.FragmentManager.OnBackStackChangedListener
29 import androidx.fragment.app.FragmentTransaction
30 import androidx.lifecycle.Lifecycle
31 import androidx.lifecycle.LifecycleEventObserver
32 import androidx.lifecycle.ViewModel
33 import androidx.lifecycle.ViewModelProvider
34 import androidx.lifecycle.viewmodel.CreationExtras
35 import androidx.lifecycle.viewmodel.initializer
36 import androidx.lifecycle.viewmodel.viewModelFactory
37 import androidx.navigation.NavBackStackEntry
38 import androidx.navigation.NavController
39 import androidx.navigation.NavDestination
40 import androidx.navigation.NavOptions
41 import androidx.navigation.Navigator
42 import androidx.navigation.NavigatorProvider
43 import androidx.navigation.NavigatorState
44 import androidx.navigation.fragment.FragmentNavigator.Destination
45 import java.lang.ref.WeakReference
46 
47 /**
48  * Navigator that navigates through [fragment transactions][FragmentTransaction]. Every destination
49  * using this Navigator must set a valid Fragment class name with `android:name` or
50  * [Destination.setClassName].
51  *
52  * The current Fragment from FragmentNavigator's perspective can be retrieved by calling
53  * [FragmentManager.getPrimaryNavigationFragment] with the FragmentManager passed to this
54  * FragmentNavigator.
55  *
56  * Note that the default implementation does Fragment transactions asynchronously, so the current
57  * Fragment will not be available immediately (i.e., in callbacks to
58  * [NavController.OnDestinationChangedListener]).
59  *
60  * FragmentNavigator respects [Log.isLoggable] for debug logging, allowing you to use `adb shell
61  * setprop log.tag.FragmentNavigator VERBOSE`.
62  */
63 @Navigator.Name("fragment")
64 public open class FragmentNavigator(
65     private val context: Context,
66     private val fragmentManager: FragmentManager,
67     private val containerId: Int
68 ) : Navigator<Destination>() {
69     // Logging for FragmentNavigator is automatically enabled along with FragmentManager logging.
70     // see more at [Debug your fragments][https://developer.android.com/guide/fragments/debugging]
71     private fun isLoggingEnabled(level: Int): Boolean {
72         return Log.isLoggable("FragmentManager", level) || Log.isLoggable(TAG, level)
73     }
74 
75     private val savedIds = mutableSetOf<String>()
76 
77     /**
78      * A list of pending operations within a Transaction expected to be executed by FragmentManager.
79      * Pending ops are added at the start of a transaction, and by the time a transaction completes,
80      * this list is expected to be cleared.
81      *
82      * In general, each entry would be added only once to this list within a single transaction
83      * except in the case of singleTop transactions. Single top transactions involve two fragment
84      * instances with the same entry, so we would get two onBackStackChanged callbacks on the same
85      * entry.
86      *
87      * Each Pair represents the entry.id and whether this entry is getting popped
88      */
89     internal val pendingOps = mutableListOf<Pair<String, Boolean>>()
90 
91     /** Get the back stack from the [state]. */
92     internal val backStack
93         get() = state.backStack
94 
95     private val fragmentObserver = LifecycleEventObserver { source, event ->
96         if (event == Lifecycle.Event.ON_DESTROY) {
97             val fragment = source as Fragment
98             val entry =
99                 state.transitionsInProgress.value.lastOrNull { entry -> entry.id == fragment.tag }
100             if (entry != null) {
101                 if (isLoggingEnabled(Log.VERBOSE)) {
102                     Log.v(
103                         TAG,
104                         "Marking transition complete for entry $entry " +
105                             "due to fragment $source lifecycle reaching DESTROYED"
106                     )
107                 }
108                 state.markTransitionComplete(entry)
109             }
110         }
111     }
112 
113     private val fragmentViewObserver = { entry: NavBackStackEntry ->
114         LifecycleEventObserver { owner, event ->
115             // Once the lifecycle reaches RESUMED, if the entry is in the back stack we can mark
116             // the transition complete
117             if (event == Lifecycle.Event.ON_RESUME && state.backStack.value.contains(entry)) {
118                 if (isLoggingEnabled(Log.VERBOSE)) {
119                     Log.v(
120                         TAG,
121                         "Marking transition complete for entry $entry due " +
122                             "to fragment $owner view lifecycle reaching RESUMED"
123                     )
124                 }
125                 state.markTransitionComplete(entry)
126             }
127             // Once the lifecycle reaches DESTROYED, we can mark the transition complete
128             if (event == Lifecycle.Event.ON_DESTROY) {
129                 if (isLoggingEnabled(Log.VERBOSE)) {
130                     Log.v(
131                         TAG,
132                         "Marking transition complete for entry $entry due " +
133                             "to fragment $owner view lifecycle reaching DESTROYED"
134                     )
135                 }
136                 state.markTransitionComplete(entry)
137             }
138         }
139     }
140 
141     override fun onAttach(state: NavigatorState) {
142         super.onAttach(state)
143         if (isLoggingEnabled(Log.VERBOSE)) {
144             Log.v(TAG, "onAttach")
145         }
146 
147         fragmentManager.addFragmentOnAttachListener { _, fragment ->
148             val entry = state.backStack.value.lastOrNull { it.id == fragment.tag }
149             if (isLoggingEnabled(Log.VERBOSE)) {
150                 Log.v(
151                     TAG,
152                     "Attaching fragment $fragment associated with entry " +
153                         "$entry to FragmentManager $fragmentManager"
154                 )
155             }
156             if (entry != null) {
157                 attachObservers(entry, fragment)
158                 // We need to ensure that if the fragment has its state saved and then that state
159                 // later cleared without the restoring the fragment that we also clear the state
160                 // of the associated entry.
161                 attachClearViewModel(fragment, entry, state)
162             }
163         }
164 
165         fragmentManager.addOnBackStackChangedListener(
166             object : OnBackStackChangedListener {
167                 override fun onBackStackChanged() {}
168 
169                 override fun onBackStackChangeStarted(fragment: Fragment, pop: Boolean) {
170                     // We only care about the pop case here since in the navigate case by the time
171                     // we get here the fragment will have already been moved to STARTED.
172                     // In the case of a pop, we move the entries to STARTED
173                     if (pop) {
174                         val entry = state.backStack.value.lastOrNull { it.id == fragment.tag }
175                         if (isLoggingEnabled(Log.VERBOSE)) {
176                             Log.v(
177                                 TAG,
178                                 "OnBackStackChangedStarted for fragment " +
179                                     "$fragment associated with entry $entry"
180                             )
181                         }
182                         entry?.let { state.prepareForTransition(it) }
183                     }
184                 }
185 
186                 override fun onBackStackChangeCommitted(fragment: Fragment, pop: Boolean) {
187                     val entry =
188                         (state.backStack.value + state.transitionsInProgress.value).lastOrNull {
189                             it.id == fragment.tag
190                         }
191 
192                     // In case of system back, all pending transactions are executed before handling
193                     // back press, hence pendingOps will be empty.
194                     val isSystemBack = pop && pendingOps.isEmpty() && fragment.isRemoving
195                     val op = pendingOps.firstOrNull { it.first == fragment.tag }
196                     op?.let { pendingOps.remove(it) }
197 
198                     if (!isSystemBack && isLoggingEnabled(Log.VERBOSE)) {
199                         Log.v(
200                             TAG,
201                             "OnBackStackChangedCommitted for fragment " +
202                                 "$fragment associated with entry $entry"
203                         )
204                     }
205 
206                     val popOp = op?.second == true
207                     if (!pop && !popOp) {
208                         requireNotNull(entry) {
209                             "The fragment " +
210                                 fragment +
211                                 " is unknown to the FragmentNavigator. " +
212                                 "Please use the navigate() function to add fragments to the " +
213                                 "FragmentNavigator managed FragmentManager."
214                         }
215                     }
216                     if (entry != null) {
217                         // In case we get a fragment that was never attached to the fragment
218                         // manager,
219                         // we need to make sure we still return the entries to their proper final
220                         // state.
221                         attachClearViewModel(fragment, entry, state)
222                         // This is the case of system back where we will need to make the call to
223                         // popBackStack. Otherwise, popBackStack was called directly and we avoid
224                         // popping again.
225                         if (isSystemBack) {
226                             if (isLoggingEnabled(Log.VERBOSE)) {
227                                 Log.v(
228                                     TAG,
229                                     "OnBackStackChangedCommitted for fragment $fragment " +
230                                         "popping associated entry $entry via system back"
231                                 )
232                             }
233                             state.popWithTransition(entry, false)
234                         }
235                     }
236                 }
237             }
238         )
239     }
240 
241     private fun attachObservers(entry: NavBackStackEntry, fragment: Fragment) {
242         fragment.viewLifecycleOwnerLiveData.observe(fragment) { owner ->
243             // attach observer unless it was already popped at this point
244             // we get onBackStackStackChangedCommitted callback for an executed navigate where we
245             // remove incoming fragment from pendingOps before ATTACH so the listener will still
246             // be added
247             val isPending = pendingOps.any { it.first == fragment.tag }
248             if (owner != null && !isPending) {
249                 val viewLifecycle = fragment.viewLifecycleOwner.lifecycle
250                 // We only need to add observers while the viewLifecycle has not reached a final
251                 // state
252                 if (viewLifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
253                     viewLifecycle.addObserver(fragmentViewObserver(entry))
254                 }
255             }
256         }
257         fragment.lifecycle.addObserver(fragmentObserver)
258     }
259 
260     internal fun attachClearViewModel(
261         fragment: Fragment,
262         entry: NavBackStackEntry,
263         state: NavigatorState
264     ) {
265         val viewModel =
266             ViewModelProvider(
267                 fragment.viewModelStore,
268                 viewModelFactory { initializer { ClearEntryStateViewModel() } },
269                 CreationExtras.Empty
270             )[ClearEntryStateViewModel::class.java]
271         viewModel.completeTransition = WeakReference {
272             entry.let {
273                 state.transitionsInProgress.value.forEach { entry ->
274                     if (isLoggingEnabled(Log.VERBOSE)) {
275                         Log.v(
276                             TAG,
277                             "Marking transition complete for entry " +
278                                 "$entry due to fragment $fragment viewmodel being cleared"
279                         )
280                     }
281                     state.markTransitionComplete(entry)
282                 }
283             }
284         }
285     }
286 
287     /**
288      * {@inheritDoc}
289      *
290      * This method must call [FragmentTransaction.setPrimaryNavigationFragment] if the pop succeeded
291      * so that the newly visible Fragment can be retrieved with
292      * [FragmentManager.getPrimaryNavigationFragment].
293      *
294      * Note that the default implementation pops the Fragment asynchronously, so the newly visible
295      * Fragment from the back stack is not instantly available after this call completes.
296      */
297     override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
298         if (fragmentManager.isStateSaved) {
299             Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already saved its state")
300             return
301         }
302         val beforePopList = state.backStack.value
303         // Get the set of entries that are going to be popped
304         val popUpToIndex = beforePopList.indexOf(popUpTo)
305         val poppedList = beforePopList.subList(popUpToIndex, beforePopList.size)
306         val initialEntry = beforePopList.first()
307 
308         // add pending ops here before any animation (if present) or FragmentManager work starts
309         val incomingEntry = beforePopList.elementAtOrNull(popUpToIndex - 1)
310         if (incomingEntry != null) {
311             addPendingOps(incomingEntry.id)
312         }
313         poppedList
314             .filter { entry ->
315                 // normally we don't add initialEntry to pending ops because the adding/popping
316                 // of an isolated fragment does not trigger onBackStackCommitted. But if initial
317                 // entry was already added to pendingOps, it was likely an incomingEntry that now
318                 // needs to be popped, so we need to overwrite isPop to true here.
319                 pendingOps.asSequence().map { it.first }.contains(entry.id) ||
320                     entry.id != initialEntry.id
321             }
322             .forEach { entry -> addPendingOps(entry.id, isPop = true) }
323         if (savedState) {
324             // Now go through the list in reversed order (i.e., started from the most added)
325             // and save the back stack state of each.
326             for (entry in poppedList.reversed()) {
327                 if (entry == initialEntry) {
328                     Log.i(
329                         TAG,
330                         "FragmentManager cannot save the state of the initial destination $entry"
331                     )
332                 } else {
333                     fragmentManager.saveBackStack(entry.id)
334                     savedIds += entry.id
335                 }
336             }
337         } else {
338             fragmentManager.popBackStack(popUpTo.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
339         }
340         if (isLoggingEnabled(Log.VERBOSE)) {
341             Log.v(
342                 TAG,
343                 "Calling popWithTransition via popBackStack() on entry " +
344                     "$popUpTo with savedState $savedState"
345             )
346         }
347 
348         state.popWithTransition(popUpTo, savedState)
349     }
350 
351     public override fun createDestination(): Destination {
352         return Destination(this)
353     }
354 
355     /**
356      * Instantiates the Fragment via the FragmentManager's [androidx.fragment.app.FragmentFactory].
357      *
358      * Note that this method is **not** responsible for calling [Fragment.setArguments] on the
359      * returned Fragment instance.
360      *
361      * @param context Context providing the correct [ClassLoader]
362      * @param fragmentManager FragmentManager the Fragment will be added to
363      * @param className The Fragment to instantiate
364      * @param args The Fragment's arguments, if any
365      * @return A new fragment instance.
366      */
367     @Suppress("DeprecatedCallableAddReplaceWith")
368     @Deprecated(
369         """Set a custom {@link androidx.fragment.app.FragmentFactory} via
370       {@link FragmentManager#setFragmentFactory(FragmentFactory)} to control
371       instantiation of Fragments."""
372     )
373     public open fun instantiateFragment(
374         context: Context,
375         fragmentManager: FragmentManager,
376         className: String,
377         args: Bundle?
378     ): Fragment {
379         return fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
380     }
381 
382     /**
383      * {@inheritDoc}
384      *
385      * This method should always call [FragmentTransaction.setPrimaryNavigationFragment] so that the
386      * Fragment associated with the new destination can be retrieved with
387      * [FragmentManager.getPrimaryNavigationFragment].
388      *
389      * Note that the default implementation commits the new Fragment asynchronously, so the new
390      * Fragment is not instantly available after this call completes.
391      *
392      * This call will be ignored if the FragmentManager state has already been saved.
393      */
394     override fun navigate(
395         entries: List<NavBackStackEntry>,
396         navOptions: NavOptions?,
397         navigatorExtras: Navigator.Extras?
398     ) {
399         if (fragmentManager.isStateSaved) {
400             Log.i(TAG, "Ignoring navigate() call: FragmentManager has already saved its state")
401             return
402         }
403         for (entry in entries) {
404             navigate(entry, navOptions, navigatorExtras)
405         }
406     }
407 
408     private fun navigate(
409         entry: NavBackStackEntry,
410         navOptions: NavOptions?,
411         navigatorExtras: Navigator.Extras?
412     ) {
413         val initialNavigation = state.backStack.value.isEmpty()
414         val restoreState =
415             (navOptions != null &&
416                 !initialNavigation &&
417                 navOptions.shouldRestoreState() &&
418                 savedIds.remove(entry.id))
419         if (restoreState) {
420             // Restore back stack does all the work to restore the entry
421             fragmentManager.restoreBackStack(entry.id)
422             state.pushWithTransition(entry)
423             return
424         }
425         val ft = createFragmentTransaction(entry, navOptions)
426 
427         if (!initialNavigation) {
428             val outgoingEntry = state.backStack.value.lastOrNull()
429             // if outgoing entry is initial entry, FragmentManager still triggers onBackStackChange
430             // callback for it, so we don't filter out initial entry here
431             if (outgoingEntry != null) {
432                 addPendingOps(outgoingEntry.id)
433             }
434             // add pending ops here before any animation (if present) starts
435             addPendingOps(entry.id)
436             ft.addToBackStack(entry.id)
437         }
438 
439         if (navigatorExtras is Extras) {
440             for ((key, value) in navigatorExtras.sharedElements) {
441                 ft.addSharedElement(key, value)
442             }
443         }
444         ft.commit()
445         // The commit succeeded, update our view of the world
446         if (isLoggingEnabled(Log.VERBOSE)) {
447             Log.v(TAG, "Calling pushWithTransition via navigate() on entry $entry")
448         }
449         state.pushWithTransition(entry)
450     }
451 
452     /**
453      * {@inheritDoc}
454      *
455      * This method should always call [FragmentTransaction.setPrimaryNavigationFragment] so that the
456      * Fragment associated with the new destination can be retrieved with
457      * [FragmentManager.getPrimaryNavigationFragment].
458      *
459      * Note that the default implementation commits the new Fragment asynchronously, so the new
460      * Fragment is not instantly available after this call completes.
461      *
462      * This call will be ignored if the FragmentManager state has already been saved.
463      */
464     override fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) {
465         if (fragmentManager.isStateSaved) {
466             Log.i(
467                 TAG,
468                 "Ignoring onLaunchSingleTop() call: FragmentManager has already saved its state"
469             )
470             return
471         }
472         val ft = createFragmentTransaction(backStackEntry, null)
473         val backstack = state.backStack.value
474         if (backstack.size > 1) {
475             // If the Fragment to be replaced is on the FragmentManager's
476             // back stack, a simple replace() isn't enough so we
477             // remove it from the back stack and put our replacement
478             // on the back stack in its place
479             val incomingEntry = backstack.elementAtOrNull(backstack.lastIndex - 1)
480             if (incomingEntry != null) {
481                 addPendingOps(incomingEntry.id)
482             }
483             addPendingOps(backStackEntry.id, isPop = true)
484             fragmentManager.popBackStack(
485                 backStackEntry.id,
486                 FragmentManager.POP_BACK_STACK_INCLUSIVE
487             )
488 
489             addPendingOps(backStackEntry.id, deduplicate = false)
490             ft.addToBackStack(backStackEntry.id)
491         }
492         ft.commit()
493         // The commit succeeded, update our view of the world
494         state.onLaunchSingleTop(backStackEntry)
495     }
496 
497     private fun createFragmentTransaction(
498         entry: NavBackStackEntry,
499         navOptions: NavOptions?
500     ): FragmentTransaction {
501         val destination = entry.destination as Destination
502         val args = entry.arguments
503         var className = destination.className
504         if (className[0] == '.') {
505             className = context.packageName + className
506         }
507         val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
508         frag.arguments = args
509         val ft = fragmentManager.beginTransaction()
510         var enterAnim = navOptions?.enterAnim ?: -1
511         var exitAnim = navOptions?.exitAnim ?: -1
512         var popEnterAnim = navOptions?.popEnterAnim ?: -1
513         var popExitAnim = navOptions?.popExitAnim ?: -1
514         if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
515             enterAnim = if (enterAnim != -1) enterAnim else 0
516             exitAnim = if (exitAnim != -1) exitAnim else 0
517             popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
518             popExitAnim = if (popExitAnim != -1) popExitAnim else 0
519             ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
520         }
521         ft.replace(containerId, frag, entry.id)
522         ft.setPrimaryNavigationFragment(frag)
523         ft.setReorderingAllowed(true)
524         return ft
525     }
526 
527     public override fun onSaveState(): Bundle? {
528         if (savedIds.isEmpty()) {
529             return null
530         }
531         return bundleOf(KEY_SAVED_IDS to ArrayList(savedIds))
532     }
533 
534     public override fun onRestoreState(savedState: Bundle) {
535         val savedIds = savedState.getStringArrayList(KEY_SAVED_IDS)
536         if (savedIds != null) {
537             this.savedIds.clear()
538             this.savedIds += savedIds
539         }
540     }
541 
542     /**
543      * NavDestination specific to [FragmentNavigator]
544      *
545      * Construct a new fragment destination. This destination is not valid until you set the
546      * Fragment via [setClassName].
547      *
548      * @param fragmentNavigator The [FragmentNavigator] which this destination will be associated
549      *   with. Generally retrieved via a [NavController]'s [NavigatorProvider.getNavigator] method.
550      */
551     @NavDestination.ClassType(Fragment::class)
552     public open class Destination
553     public constructor(fragmentNavigator: Navigator<out Destination>) :
554         NavDestination(fragmentNavigator) {
555 
556         /**
557          * Construct a new fragment destination. This destination is not valid until you set the
558          * Fragment via [setClassName].
559          *
560          * @param navigatorProvider The [NavController] which this destination will be associated
561          *   with.
562          */
563         public constructor(
564             navigatorProvider: NavigatorProvider
565         ) : this(navigatorProvider.getNavigator(FragmentNavigator::class.java))
566 
567         @CallSuper
568         public override fun onInflate(context: Context, attrs: AttributeSet) {
569             super.onInflate(context, attrs)
570             context.resources.obtainAttributes(attrs, R.styleable.FragmentNavigator).use { array ->
571                 val className = array.getString(R.styleable.FragmentNavigator_android_name)
572                 if (className != null) setClassName(className)
573             }
574         }
575 
576         /**
577          * Set the Fragment class name associated with this destination
578          *
579          * @param className The class name of the Fragment to show when you navigate to this
580          *   destination
581          * @return this [Destination]
582          */
583         public fun setClassName(className: String): Destination {
584             _className = className
585             return this
586         }
587 
588         private var _className: String? = null
589         /**
590          * The Fragment's class name associated with this destination
591          *
592          * @throws IllegalStateException when no Fragment class was set.
593          */
594         public val className: String
595             get() {
596                 checkNotNull(_className) { "Fragment class was not set" }
597                 return _className as String
598             }
599 
600         public override fun toString(): String {
601             val sb = StringBuilder()
602             sb.append(super.toString())
603             sb.append(" class=")
604             if (_className == null) {
605                 sb.append("null")
606             } else {
607                 sb.append(_className)
608             }
609             return sb.toString()
610         }
611 
612         override fun equals(other: Any?): Boolean {
613             if (this === other) return true
614             if (other == null || other !is Destination) return false
615             return super.equals(other) && _className == other._className
616         }
617 
618         override fun hashCode(): Int {
619             var result = super.hashCode()
620             result = 31 * result + _className.hashCode()
621             return result
622         }
623     }
624 
625     /** Extras that can be passed to FragmentNavigator to enable Fragment specific behavior */
626     public class Extras internal constructor(sharedElements: Map<View, String>) : Navigator.Extras {
627         private val _sharedElements = LinkedHashMap<View, String>()
628 
629         /**
630          * The map of shared elements associated with these Extras. The returned map is an
631          * [unmodifiable][Map] copy of the underlying map and should be treated as immutable.
632          */
633         public val sharedElements: Map<View, String>
634             get() = _sharedElements.toMap()
635 
636         /**
637          * Builder for constructing new [Extras] instances. The resulting instances are immutable.
638          */
639         public class Builder {
640             private val _sharedElements = LinkedHashMap<View, String>()
641 
642             /**
643              * Adds multiple shared elements for mapping Views in the current Fragment to
644              * transitionNames in the Fragment being navigated to.
645              *
646              * @param sharedElements Shared element pairs to add
647              * @return this [Builder]
648              */
649             public fun addSharedElements(sharedElements: Map<View, String>): Builder {
650                 for ((view, name) in sharedElements) {
651                     addSharedElement(view, name)
652                 }
653                 return this
654             }
655 
656             /**
657              * Maps the given View in the current Fragment to the given transition name in the
658              * Fragment being navigated to.
659              *
660              * @param sharedElement A View in the current Fragment to match with a View in the
661              *   Fragment being navigated to.
662              * @param name The transitionName of the View in the Fragment being navigated to that
663              *   should be matched to the shared element.
664              * @return this [Builder]
665              * @see FragmentTransaction.addSharedElement
666              */
667             public fun addSharedElement(sharedElement: View, name: String): Builder {
668                 _sharedElements[sharedElement] = name
669                 return this
670             }
671 
672             /**
673              * Constructs the final [Extras] instance.
674              *
675              * @return An immutable [Extras] instance.
676              */
677             public fun build(): Extras {
678                 return Extras(_sharedElements)
679             }
680         }
681 
682         init {
683             _sharedElements.putAll(sharedElements)
684         }
685     }
686 
687     private companion object {
688         private const val TAG = "FragmentNavigator"
689         private const val KEY_SAVED_IDS = "androidx-nav-fragment:navigator:savedIds"
690     }
691 
692     internal class ClearEntryStateViewModel : ViewModel() {
693         lateinit var completeTransition: WeakReference<() -> Unit>
694 
695         override fun onCleared() {
696             super.onCleared()
697             completeTransition.get()?.invoke()
698         }
699     }
700 
701     /**
702      * In general, each entry would only get one callback within a transaction except for single top
703      * transactions, where we would get two callbacks for the same entry.
704      */
705     private fun addPendingOps(id: String, isPop: Boolean = false, deduplicate: Boolean = true) {
706         if (deduplicate) {
707             pendingOps.removeAll { it.first == id }
708         }
709         pendingOps.add(id to isPop)
710     }
711 }
712