背景/概要
最近FragmentベースのNavigation Componentの導入を再検討しています。
数年前はXMLでNavGraph
を作成し、action
で画面遷移を定義するのが一般的でしたが、バージョン 2.4.0からナビゲーションルートとKotlin DSLによる動的なグラフ作成が導入されました。
また、ナビゲーションルートでの画面遷移は暗黙的DeepLinkによるものになります。
この時のFragmentのスタック管理方法が気になったため調査します。
※現時点の安定バージョンはバージョン 2.7.7
コードリーディング
ナビゲーションルートでの画面遷移でもNavController#navigate
を呼び出すのは変わりません。
引数にroute
をとるメソッドを利用します。
public fun navigate(route: String, builder: NavOptionsBuilder.() -> Unit) { navigate(route, navOptions(builder)) } public fun navigate( route: String, navOptions: NavOptions? = null, navigatorExtras: Navigator.Extras? = null ) { navigate( NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build(), navOptions, navigatorExtras ) }
2つ目のnavigate
メソッドでさらにnavigate
メソッドを呼び出していますが、引数にNavDeepLinkRequest
を渡しているのがわかります。
つまりは暗黙的DeepLinkが利用されています。
これはバージョン 2.7.7のリリースノートでも言及されています。
ID によるナビゲーションとは異なり、ルートによるナビゲーションは暗黙的ディープリンクと同じルールに従います。
ちなみに明示的DeepLinkを作成する場合は、NavDeepLinkBuilder
を利用します。
参考:
デスティネーションへのディープリンクを作成する | Android Developers
またcreateRoute(route)
では"android-app://androidx.navigation/$route"
という文字列を返し、それをDeepLink用のUri
に変換しています。
次に引数にNavDeepLinkRequest
をとるnavigate
メソッドを見てみます。
@MainThread public open fun navigate( request: NavDeepLinkRequest, navOptions: NavOptions?, navigatorExtras: Navigator.Extras? ) { requireNotNull(_graph) { "Cannot navigate to $request. Navigation graph has not been set for " + "NavController $this." } val deepLinkMatch = _graph!!.matchDeepLink(request) if (deepLinkMatch != null) { val destination = deepLinkMatch.destination val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle() val node = deepLinkMatch.destination val intent = Intent().apply { setDataAndType(request.uri, request.mimeType) action = request.action } args.putParcelable(KEY_DEEP_LINK_INTENT, intent) navigate(node, args, navOptions, navigatorExtras) } else { throw IllegalArgumentException( "Navigation destination that matches request $request cannot be found in the " + "navigation graph $_graph" ) } }
まずNavGraph#matchDeepLink
でrequest
に対応するDeepLinkを探索します。
そこで見つからない場合はIllegalArgumentException
がスローされます。
見つかった場合はDeepLink用の引数やIntent
を作成し、さらにprivate
なnavigate
メソッドを呼び出します。
@MainThread private fun navigate( node: NavDestination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras? ) { navigatorState.values.forEach { state -> state.isNavigating = true } var popped = false var launchSingleTop = false var navigated = false if (navOptions != null) { when { navOptions.popUpToRoute != null -> popped = popBackStackInternal( navOptions.popUpToRoute!!, navOptions.isPopUpToInclusive(), navOptions.shouldPopUpToSaveState() ) navOptions.popUpToId != -1 -> popped = popBackStackInternal( navOptions.popUpToId, navOptions.isPopUpToInclusive(), navOptions.shouldPopUpToSaveState() ) } } val finalArgs = node.addInDefaultArgs(args) // Now determine what new destinations we need to add to the back stack if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) { navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras) } else { launchSingleTop = navOptions?.shouldLaunchSingleTop() == true && launchSingleTopInternal(node, args) if (!launchSingleTop) { // Not a single top operation, so we're looking to add the node to the back stack val backStackEntry = NavBackStackEntry.create( context, node, finalArgs, hostLifecycleState, viewModel ) val navigator = _navigatorProvider.getNavigator<Navigator<NavDestination>>( node.navigatorName ) navigator.navigateInternal(listOf(backStackEntry), navOptions, navigatorExtras) { navigated = true addEntryToBackStack(node, finalArgs, it) } } } updateOnBackPressedCallbackEnabled() navigatorState.values.forEach { state -> state.isNavigating = false } if (popped || navigated || launchSingleTop) { dispatchOnDestinationChanged() } else { updateBackStackLifecycle() } }
private
なnavigate
メソッドではようやくバックスタックの更新をします。
はじめにnavigatorState
で保持しているNavigator
の状態を更新しています。
カスタムのNavigator
を使っていなければ、navigatorState
はNavGraphNavigator
・ActivityNavigator
・DialogFragmentNavigator
・FragmentNavigator
を持っています。
次にnavOptions
の内容を処理しています。
- popUpToRoute
- popUpToId
- shouldRestoreState()
- shouldLaunchSingleTop()
launchSingleTop == false
の場合は、新しいバックスタックを作成し、navigator.navigateInternal(...)
を呼び出しています。
navigator.navigateInternal(...)
でバックスタックを引数にとるnavigate
メソッドを呼び出します。
private fun Navigator<out NavDestination>.navigateInternal( entries: List<NavBackStackEntry>, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?, handler: (backStackEntry: NavBackStackEntry) -> Unit = {} ) { addToBackStackHandler = handler navigate(entries, navOptions, navigatorExtras) addToBackStackHandler = null }
バックスタックを引数にとるnavigate
メソッドはopen
になっていて、FragmentNavigator
などでoverrideされています。
ちなみにActivityNavigator
だけはoverride
していません。
public open fun navigate( entries: List<NavBackStackEntry>, navOptions: NavOptions?, navigatorExtras: Extras? ) { entries.asSequence().map { backStackEntry -> val destination = backStackEntry.destination as? D ?: return@map null val navigatedToDestination = navigate( destination, backStackEntry.arguments, navOptions, navigatorExtras ) when (navigatedToDestination) { null -> null destination -> backStackEntry else -> { state.createBackStackEntry( navigatedToDestination, navigatedToDestination.addInDefaultArgs(backStackEntry.arguments) ) } } }.filterNotNull().forEach { backStackEntry -> state.push(backStackEntry) } }
overrideされたNavigator#navigate
の処理はまた別の記事で調査します。
まとめ
- ルートによるナビゲーションは暗黙的ディープリンクと同じルール
- 具体的なナビゲーション処理は
Navigator
を実装しているFragmentNavigator
などに移譲されている