Open9

Navigation Composeめも

eanean

saveState = true を設定すると何が起きるか?

popUpTo(id) {
    saveState = true
}
eanean

PopUpToBuilderのsaveStateがtrueになり、
それをNavOptionBuilderのsaveStateをtrueにする

// NavOptionsBuilder
public fun popUpTo(@IdRes id: Int, popUpToBuilder: PopUpToBuilder.() -> Unit = {}) {
    val builder = PopUpToBuilder().apply(popUpToBuilder)
    saveState = builder.saveState
}
eanean

saveStateがtrueのnavOptionsを渡すと

navController.navigate(route, navOptions)

辿っていくと以下のシグネチャのnavigate関数までたどり着く(navigateという同名関数が多い)

@MainThread
private fun navigate(
    node: NavDestination,
    args: Bundle?,
    navOptions: NavOptions?,
    navigatorExtras: Navigator.Extras?
) {}
eanean

読み進めると以下でpopしてるっぽい

popped = popBackStackInternal(
    navOptions.popUpToId,
    navOptions.isPopUpToInclusive(),
    navOptions.shouldPopUpToSaveState()
)

NavOption.popUpToSaveStateはprivateでshouldPopUpToSaveState()で公開されているが、
shouldPopUpToSaveState()はここでしか使用されていない。
つまり、popBackStackInternalを確認すれば何をしてるか分かりそう

eanean

popUpToで指定したIDまでのNavigatorをpopOperationsに格納します。
指定したidが見つかった時点でループを抜けます。

// popBackStackInternal
val popOperations = mutableListOf<Navigator<*>>()
val iterator = backQueue.reversed().iterator()
var foundDestination: NavDestination? = null
while (iterator.hasNext()) {
    val destination = iterator.next().destination
    val navigator = _navigatorProvider.getNavigator<Navigator<*>>(
        destination.navigatorName
    )
    if (inclusive || destination.id != destinationId) {
        popOperations.add(navigator)
    }
    if (destination.id == destinationId) {
        foundDestination = destination
        break
    }
}
eanean

Navigatorのセットは

NavControllerが作られた時と

init {
    _navigatorProvider.addNavigator(NavGraphNavigator(_navigatorProvider))
    _navigatorProvider.addNavigator(ActivityNavigator(context))
}

rememberから作られた時はこちらも追加されている模様

private fun createNavController(context: Context) =
    NavHostController(context).apply {
        navigatorProvider.addNavigator(ComposeNavigator())
        navigatorProvider.addNavigator(DialogNavigator())
    }
eanean

NavControllerは一つのNavigatorProviderを持っていて、
NavigatorProviderはNavigatorを
Map<String, Navigator>で管理している。

navController.navigatorProvider.navigatorsから実際に取得できるの見てみると実際に上でセットした値が入っている

navigation    -> androidx.navigation.NavGraphNavigator
activity         -> androidx.navigation.ActivityNavigator
composable -> androidx.navigation.compose.ComposeNavigator
dialog            -> androidx.navigation.compose.DialogNavigator

keyはNavigatorProvider.getNameForNavigatorからアノテーションの名前を取っているっぽい

eanean

つまり先ほどのpopOperationsの中には、今回はComposeなので
ComposeNavigator<それぞれのDestination>のデータが入っている。

NavController.popBackStackInternalの続きをみると、

navigator.popBackStackInternal() (NavController)

ComposeNavigator.popBackStack()

NavigationState.popWithTranstion()

NavControllerNavigatorState.popWithTransition()

ここの処理を見てみると以下のような処理がある。
つまりpopしたNavBackStackEntryをkeyにsaveStateの値を保存している。
この値を後で復元の時に使用していると思われる

// NavController
private val entrySavedState = mutableMapOf<NavBackStackEntry, Boolean>()
override fun popWithTransition(popUpTo: NavBackStackEntry, saveState: Boolean) {
    super.popWithTransition(popUpTo, saveState)
    entrySavedState[popUpTo] = saveState
}
eanean

NavigatoStateのbackStackを削除した後、そのBackStackEntryをもらい
実際にNavControllerが保持するbackQueueから削除する処理をpopEntryFromBackStackで行う。

navigator.popBackStackInternal(backQueue.last(), saveState) { entry ->
        receivedPop = true
        popped = true
        popEntryFromBackStack(entry, saveState, savedState)
}

popEntryFromBackStackの大事そうな処理の場所を見ると
saveStateがtrueの場合

  1. savedState.addFirst(NavBackStackEntryState(entry))を呼ぶ
  2. viewModel?.clear(entry.id)を終了させる

1については次に進む
2はViewModelにonClearedにログを入れれ、saveState=falseで動作確認すると、確かにpopの時にViewModelが破棄されている事がわかる。

事がわかります。

if (entry.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
    if (saveState) {
        // Move the state through STOPPED
        entry.maxLifecycle = Lifecycle.State.CREATED
        // Then save the state of the NavBackStackEntry
        savedState.addFirst(NavBackStackEntryState(entry))
    }
    if (!transitioning) {
        entry.maxLifecycle = Lifecycle.State.DESTROYED
        unlinkChildFromParent(entry)
    } else {
        entry.maxLifecycle = Lifecycle.State.CREATED
    }
}
if (!saveState && !transitioning) {
    viewModel?.clear(entry.id)
}