本頁會說明針對 Navigation Kotlin DSL 和 Navigation Compose 提供執行階段類型安全的最佳做法。總結來說,您的應用程式或導覽圖的每個畫面都應依據個別模組對應至 Navigation 檔案。產生的每個檔案都應含有指定目的地的所有 Navigation 相關資訊。這些導覽檔案也是 Kotlin 瀏覽權限修飾詞用來提供執行階段類型安全的地方:
- 系統會將類型安全函式公開揭露給程式碼集的其餘部分。
- 特定畫面或導覽圖的 Navigation 專屬概念會放在同一檔案中,並維持不公開,確保程式碼集的基餘部分無法存取。
分割導覽圖
建議您按畫面分割導覽圖。這和將畫面分割成不同可組合函式的方法大致相同。每個畫面都應有 NavGraphBuilder
擴充功能函式。
這項擴充功能函式是無狀態畫面層級可組合函式和 Navigation 專屬邏輯之間的橋樑。此資料層也可定義該狀態的來源與事件的處理方式。
以下是可以設為自身 internal
模組的一般 ConversationScreen
,這樣一來其他模組就無法存取:
// ConversationScreen.kt
@Composable
internal fun ConversationScreen(
uiState: ConversationUiState,
onPinConversation: () -> Unit,
onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }
下列 NavGraphBuilder
擴充功能函式會將 ConversationScreen
可組合項新增為 NavGraph
的目的地,也會將畫面連結至提供畫面 UI 狀態的 ViewModel,並處理畫面相關的商業邏輯。無法由 ViewModel 處理的 Navigation 事件會揭露給呼叫端。
// ConversationNavigation.kt
private const val conversationIdArg = "conversationId"
// Adds conversation screen to `this` NavGraphBuilder
fun NavGraphBuilder.conversationScreen(
// Navigation events are exposed to the caller to be handled at a higher level
onNavigateToParticipantList: (conversationId: String) -> Unit
) {
composable("conversation/{$conversationIdArg}") {
// The ViewModel as a screen level state holder produces the screen
// UI state and handles business logic for the ConversationScreen
val viewModel: ConversationViewModel = hiltViewModel()
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
ConversationScreen(
uiState,
::viewModel.pinConversation,
onNavigateToParticipantList
)
}
}
ConversationNavigation.kt
檔案會將程式碼從 Navigation 程式庫與目的地本身拆分出來,還會為 Navigation 概念 (例如設為不公開的路徑或引數 ID) 提供封裝,因為這些概念不得洩露到這個檔案之外。無法在這個資料層處理的 Navigation 事件必須向呼叫端揭露,系統才能在正確的層級處理這些事件。您可以在上方程式碼片段中找到包含 onNavigateToParticipantList
的事件範例。
類型安全導覽
Navigation Compose 的建構基礎 Navigation Kotlin DSL「目前」不提供 Safe Args 提供給內建於導覽 XML 資源檔案中的導覽圖的那種編譯時間類型安全。Safe Args 會產生程式碼,其中包含針對 Navigation 目的地和動作的類型安全類別和方法。不過,您可以在執行階段將 Navigation 程式碼建構為類型安全。這樣做可避免當機,同時確保下列事項:
- 前往目的地或導覽圖時,您提供的引數會是正確類型,且所有必要引數都會呈現。
- 您從
SavedStateHandle
擷取的引數是正確的類型。
前往目的地
每個目的地也都應該揭露 NavController
擴充功能函式,以便讓其他目的地能安全地前往該目的地。
// ConversationNavigation.kt
fun NavController.navigateToConversation(conversationId: String) {
this.navigate("conversation/$conversationId")
}
如要前往您應用程式中含有其他 NavOptions
的畫面 (例如 popUpTo, savedState, restoreState
或 singleTop
),請在導覽時傳遞選用參數至 NavController
擴充功能函式。
// HomeNavigation.kt
const val HomeRoute = "home"
fun NavController.navigateToHome(navOptions: NavOptions? = null) {
this.navigate(HomeRoute, navOptions)
}
類型安全引數包裝函式
您可以視需要建立類型安全包裝函式,以便為 ViewModel 從 SavedStateHandle
擷取引數,也從目的地內容中的 NavBackStackEntry
擷取引數,藉此獲得本章節簡介中提供的好處。
// ConversationNavigation.kt
private const val conversationIdArg = "conversationId"
internal class ConversationArgs(val conversationId: String) {
constructor(savedStateHandle: SavedStateHandle) :
this(checkNotNull(savedStateHandle[conversationIdArg]) as String)
}
// ConversationViewModel.kt
internal class ConversationViewModel(...,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val conversationArgs = ConversationArgs(savedStateHandle)
}
組合導覽圖
導覽圖會使用上述的類型安全擴充功能函式新增目的地,並前往這些目的地。
在以下範例中,「對話」目的地和另外兩個目的地 (「主畫面」和「參與者名單」) 會一起納入到一個應用程式層級 NavHost
,如下所示:
// MyApp.kt
@Composable
fun MyApp(modifier: Modifier = Modifier) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = HomeRoute,
modifier = modifier
) {
homeScreen(
onNavigateToConversation = { conversationId ->
navController.navigateToConversation(conversationId)
}
)
conversationScreen(
onNavigateToParticipantList = { conversationId ->
navController.navigateToParticipantList(conversationId)
}
}
participantListScreen()
}
巢狀導覽圖中的類型安全
請為提供多個畫面的模組選擇正確的瀏覽權限。這個概念與以上章節中各方法的概念相同。不過,將個別畫面完全揭露給其他模組可能並不合理。在這種情況下,建議您將這些畫面視為範圍更大的獨立流程中的一部分。
這組獨立畫面稱為巢狀導覽圖,可讓您將多個畫面納入到單一 NavGraphBuilder
擴充功能方法中。這個方法接著會逐一使用這些 NavController
擴充功能方法將同一模組中的各個畫面串連在一起。
在以下範例中,上一節所述的「對話」目的地會顯示在巢狀導覽圖中,與另外兩個目的地 (「對話清單」和「參與者清單」) 並列:
// ConversationGraphNavigation.kt
private val ConversationGraphRoutePattern = "conversation"
fun NavController.navigateToConversationGraph(navOptions: NavOptions? = null) {
this.navigate(ConversationGraphRoutePattern, navOptions)
}
fun NavGraphBuilder.conversationGraph(navController: NavController) {
navigation(
startDestination = ConversationListRoutePattern,
route = ConversationGraphRoutePattern
) {
conversationListScreen(
onNavigateToConversation = { conversationId ->
navController.navigateToConversation(conversationId)
}
)
conversationScreen(
onNavigateToParticipantList = { conversationId ->
navController.navigateToParticipantList(conversationId)
}
)
partipantList()
}
您可以在應用程式層級 NavHost
中使用多個巢狀結構導覽圖,如下所示:
// MyApp.kt
@Composable
fun MyApp(modifier: Modifier = Modifier) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = HomeGraphRoutePattern
modifier = modifier
) {
homeGraph(
navController,
onNavigateToConversation = {
navController.navigateToConversationGraph()
}
}
conversationGraph(navController)
}