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