TL;DR
Navigation ComponentでNested Navigation Graphのrouteを指定してpopBackStack
したときは、inclusive
の値がtrue/falseいずれの場合もNested Navigation Graphから抜けることができる。
Nested Navigation Graph
Navigation Componentでは、Nested Navigation Graphによって特定のフローをモジュール化できます。公式ドキュメントの「Navigating with Compose」では、ログインフローをモジュール化する例が記載されています。
Nested Navigation Graphにより、特定のフローの詳細情報を隠蔽し、必要以上にDestinationを公開することなく画面遷移が行えるようになります。
ここで、以下のような画面フローがあるとします。
太枠で囲われたScreen AとScreen Cは、Main/Sub Navigation Graphのstart destinationに対応する画面です。
これを実装すると、例えば以下のように書くことができます。
val navController = rememberNavController() NavHost( navController = navController, startDestination = DestinationScreenA, ) { // main navigation graph scope composable(DestinationScreenA) { ScreenA( navigateToScreenB = { navController.navigate(DestinationScreenB) }, ) } composable(DestinationScreenB) { ScreenB( navigateToScreenC = { navController.navigateToScreenC() }, ) } subNavGraph( navController = navController, navigateBackToScreenB = { navController.popBackStack( DestinationScreenB, inclusive = false, ) }, ) ) private const val DestinationScreenA = "a" private const val DestinationScreenB = "b" // other module public fun NavGraphBuilder.subNavGraph( navController: NavController, navigateBackToScreenB: () -> Unit, ) { navigation( route = DestinationSubNavGraphRoute, startDestination = DestinationScreenC, ) { // sub navigation graph scope composable(RouteSubNavGraph) { ScreenC( navigateToScreenD = { navController.navigate(DestinationScreenD) }, ) } composable(DestinationScreenD) { ScreenD( navigateBackToB = navigateBackToScreenB, ) } } } // expose navigation extensions public fun NavController.navigateToScreenC() { navigate(DestinationScreenC) } // encapsulate destinations private const val RouteSubNavGraph = "sub_nav_graph" private const val DestinationScreenC = "c" private const val DestinationScreenD = "d"
また、Sub Navigation Graphのrouteを公開することによって、以下のように書くこともできます。差分のある行の近辺を抜粋しています。
val navController = rememberNavController() NavHost( navController = navController, startDestination = DestinationScreenA, ) { // main navigation graph scope ... composable(DestinationScreenB) { ScreenB( navigateToScreenC = { - navController.navigateToScreenC() + navController.navigate(RouteSubNavGraph) }, ) } subNavGraph( navController = navController, navigateBackToScreenB = { navController.popBackStack( - DestinationScreenB, + RouteSubNavGraph, inclusive = false, ) }, ) ) private const val DestinationScreenA = "a" private const val DestinationScreenB = "b" // other module ... // expose navigation extensions - public fun NavController.navigateToScreenC() { - navigate(DestinationScreenC) - } // encapsulate destinations - private const val RouteSubNavGraph = "sub_nav_graph" + public const val RouteSubNavGraph = "sub_nav_graph" private const val DestinationScreenC = "c" private const val DestinationScreenD = "d"
このコードにおいて、navigateBackToScreenB
内のpopBackStack
の第1引数の値にSub Navigation Graphのrouteを指定したときに、第2引数inclusive
の値にtrue/falseのどちらを指定すればいいのか分からず、そのことについて調べた結果がこの記事の内容です。
Back stackの変遷
Screen A -> Screen B -> Screen C -> Screen D -> Screen Bと画面遷移したとき、inclusive
の値がtrue/falseいずれの場合もBack stackは以下のようになっていました。
Back stackの内容は、navController.currentBackStack.collectAsState()
をログ出力して確認しています。
1. 起動後: [] (空) 2. Screen A表示後: [ NavBackStackEntry(...) destination=ComposeNavGraph(0x0) startDestination={Destination(...) route=a}, NavBackStackEntry(...) destination=Destination(...) route=a ] 3. Screen A -> Screen Bへの遷移後: [ NavBackStackEntry(...) destination=ComposeNavGraph(0x0) startDestination={Destination(...) route=a}, NavBackStackEntry(...) destination=Destination(...) route=a, NavBackStackEntry(...) destination=Destination(...) route=b ] 4. Screen B -> Screen Cへの遷移後: [ NavBackStackEntry(...) destination=ComposeNavGraph(0x0) startDestination={Destination(...) route=a}, NavBackStackEntry(...) destination=Destination(...) route=a, NavBackStackEntry(...) destination=Destination(...) route=b, * NavBackStackEntry(...) destination=ComposeNavGraph(...) route=sub_nav_graph startDestination={Destination(...) route=c}, NavBackStackEntry(...) destination=Destination(...) route=c ] 5. Screen C -> Screen Dへの遷移後: [ NavBackStackEntry(...) destination=ComposeNavGraph(0x0) startDestination={Destination(...) route=a}, NavBackStackEntry(...) destination=Destination(...) route=a, NavBackStackEntry(...) destination=Destination(...) route=b, * NavBackStackEntry(...) destination=ComposeNavGraph(...) route=sub_nav_graph startDestination={Destination(...) route=c}, NavBackStackEntry(...) destination=Destination(...) route=c, NavBackStackEntry(...) destination=Destination(...) route=d ] 6. Screen D -> Screen Bへの遷移後: [ NavBackStackEntry(...) destination=ComposeNavGraph(0x0) startDestination={Destination(...) route=a}, NavBackStackEntry(...) destination=Destination(...) route=a, NavBackStackEntry(...) destination=Destination(...) route=b ]
注目すべき点は、Nested Navigation Graph自体の情報もBack stackの一部として管理されていることです(*
で示したNavBackStackEntry)。そして、*
で示したBackStackEntryもroute=sub_nav_graph
という情報を持っています。
ここから、Nested Navigation Graphのroute
を指定してpopBackStack
すると、本来であればinclusive
の値がtrueであれば*
で示したNavBackStackEntryまで、falseであれば*
で示したNavBackStackEntryの1つ手前までpopされるであろうと考えられます。
しかしながら、実際の挙動としてはそうなっておらず、inclusive
の値がtrue/falseのいずれの場合も*
で示したNavBackStackEntryまでpopされていました。
そこで、popBackStack
の内部実装を追ってみたところ、以下のロジックがありました。
private fun dispatchOnDestinationChanged(): Boolean { // We never want to leave NavGraphs on the top of the stack while (!backQueue.isEmpty() && backQueue.last().destination is NavGraph) { popEntryFromBackStack(backQueue.last()) } ... }
このwhile文の中で、backQueue
の最後にある要素のdestination
がNavGraph
型であれば、その要素を追加でpopする操作を行っていました(実際のコード)。
これにより、inclusive
の値がfalseであっても*
で示したNavBackStackEntryが追加でpopされ、Nested Navigation Graphから抜けると分かりました。