Navigation Componentの暗黙的DeepLinkの処理を追う

背景/概要

最近FragmentベースのNavigation Componentの導入を再検討しています。

数年前はXMLNavGraphを作成し、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
        )
    }

引用:NavController.kt

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"
            )
        }
    }

引用:NavController.kt

まずNavGraph#matchDeepLinkrequestに対応するDeepLinkを探索します。
そこで見つからない場合はIllegalArgumentExceptionがスローされます。

見つかった場合はDeepLink用の引数やIntentを作成し、さらにprivatenavigateメソッドを呼び出します。

    @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()
        }
    }

引用:NavController.kt

privatenavigateメソッドではようやくバックスタックの更新をします。

はじめにnavigatorStateで保持しているNavigatorの状態を更新しています。
カスタムのNavigatorを使っていなければ、navigatorStateNavGraphNavigatorActivityNavigatorDialogFragmentNavigatorFragmentNavigatorを持っています。

次に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
    }

引用:NavController.kt

バックスタックを引数にとる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)
        }
    }

引用:NavController.kt

overrideされたNavigator#navigateの処理はまた別の記事で調査します。

まとめ

  • ルートによるナビゲーションは暗黙的ディープリンクと同じルール
  • 具体的なナビゲーション処理はNavigatorを実装しているFragmentNavigatorなどに移譲されている