這次遷移到 Compose 時特別花了時間試用 Compose 的 Navigation component,終於弄清 nested graph 的意義。其實 Compose 的 Navigation component 底層都是跟 XML 版的 Navigation component 一樣,只是底層多了以 route 形式的處理。以往的說明文件在介紹 nested navigation graph 時沒有太具體說明 nested graph 背後的意義,看完之後可能覺得只是用來避免單一 XML 檔過長而拆成不同 sub-graph。但其實在 deep link 時是有特別意義。
接下來會用例子示範 nested graph 在 navigation 時出現的不同效果。首先放上在 NavHost
各頁會用到的 Screen
composable function,它會顯示文字和按鈕,用來顯示當前頁面的 route 和按下按鈕時會轉到另一頁。
@Composable
fun Screen(
name: String,
onClick: (() -> Unit)? = null,
secondaryOnClick: (() -> Unit)? = null,
) {
Column {
Text(text = name)
if (onClick != null) {
Button(onClick = onClick) {
Text(text = "next screen")
}
}
if (secondaryOnClick != null) {
Button(onClick = secondaryOnClick) {
Text(text = "secondary button")
}
}
}
}
接下來就是 navigation graph。
val navController = rememberNavController()
NavHost(
modifier = modifier,
navController = navController,
startDestination = "top",
) {
composable(route = "top") {
Screen(
name = "top",
onClick = { navController.navigate("partA") },
secondaryOnClick = { navController.navigate("b3") },
)
}
navigation(startDestination = "a1", route = "partA") {
composable(route = "a1") {
Screen(name = "a1") { navController.navigate("a2") }
}
composable(route = "a2") {
Screen(name = "a2") { navController.navigate("a3") }
}
composable(route = "a3") {
Screen(
name = "a3",
onClick = { navController.navigate("partB") },
secondaryOnClick = {
val intents = navController.createDeepLink()
.addDestination("b3")
.createTaskStackBuilder()
.intents
navController.handleDeepLink(intents.first())
},
)
}
}
navigation(startDestination = "b1", route = "partB") {
composable(route = "b1") { Screen(name = "b1") }
navigation(startDestination = "b2", route = "partB2") {
composable(route = "b2") { Screen(name = "b2") }
composable(route = "b3") { Screen(name = "b3") }
}
}
}
先看首頁 top
,按 top
的「next screen」按鈕能進入 a1
頁,因為 a1
是 partA
sub-graph 的 start destination。其實在這個情況下可以把 onClick
lambda 寫成 navController.navigate("a1")
都是一樣效果。如果一直按「next screen」按鈕就會順序進入 a1
➔ a2
➔ a3
➔ b1
頁。按 back 掣就能由 b2
沿路回到 a1
,最後結束 Activity
(假設 NavHost
是放在 Activity
)。
這個看起來沒甚麼特別。之後換成在 top
按「secondary button」鈕能的話就會直接進入 b3
頁,按 back 掣一次就會回到 top
。
接着在 top
頁一直按「next screen」按鈕直到看到 a3
頁時按「secondary button」按鈕,你會發現它進了 b3
頁。這時你再按 back 掣就會看到它回到 b2
➔ b1
➔ top
。a3
頁的 secondaryOnClick
用了 deep link 做轉頁,所以當前的 back stack 會被清空,然後換成適用於 b3
頁的 back stack。而這個 back stack 的生成方法是看 b3
頁所屬的 sub-graph (partB2
) 的 start destination (b2
),所以 b3
的上一頁是 b2
頁。而 b2
頁的上一頁就是看它所屬的 sub-graph (partB
) 的 start destination (b1
)。按照同一原理 b2
頁的上一頁就是 top
頁。
如果我們把 top
的 secondaryOnClick
改為用 deep link 形式進入 a3
頁(即是改成下面的 code),你會看到它的 back stack 是 a3
➔ a1
➔ top
。沒有 a2
是因為 a3
的 sub-graph partA
的 start destination 是 a1
。換句話說 a1
、a2
、a3
都是平級。
composable(route = "top") {
Screen(
name = "top",
onClick = { navController.navigate("partA") },
secondaryOnClick = {
val intents = navController.createDeepLink()
.addDestination("a3")
.createTaskStackBuilder()
.intents
navController.handleDeepLink(intents.first())
},
)
}
所以 sub-graph 的作用是在 deep linking 時控制 back stack,這種寫法是確保 Navigation component 能掌握該頁的上一頁,並且只有一頁能做上一頁(簡單來說是退路)。如果不是 deep linking 的話其實你可以隨便轉去整個 navigation graph 內的任何一頁,只要你知道它的 route 就可以了。另外,deep linking 不一定要配 URI(要在 AndroidManifest.xml 加上 intent-filter
那種),剛才例子的寫法是不用加 intent-filter
。如果你要在某個很深層的頁面跳轉去另一個深層的頁面,用這個寫法會比每次特意控制 pop back stack 更加自然。當然,如果你是用 URI 那種 deep linking 的話,sub-graph 就是控制 deep link 開 app 後所造出來的 back stack。