このページでは、Navigation Kotlin DSL と Navigation Compose において、ランタイムでの型安全性を実現するためのベスト プラクティスについて説明します。ポイントは、アプリまたはナビゲーション グラフの各画面を、モジュールごとに Navigation ファイルにマッピングする必要があるということです。マッピングされたファイルには、それぞれ特定のデスティネーションに関する Navigation 関連の情報がすべて含まれていなければなりません。Kotlin の可視性修飾子によって、ナビゲーション ファイルでもランタイムでの型安全性が実現されます。
- 型安全な関数は、他のコードベースに対して公開されます。
- 特定の画面やナビゲーション グラフに対するナビゲーション固有のコンセプトは、同じファイル内に配置して非公開にし、他のコードベースからはアクセスできなくします。
ナビゲーション グラフを分割する
ナビゲーション グラフは画面ごとに分割する必要があります。これは、画面を複数のコンポーズ可能な関数に分割する場合と基本的に同じです。各画面には NavGraphBuilder
拡張関数が必要です。
この拡張関数は、画面レベルでコンポーズ可能なステートレス関数と Navigation 固有のロジックを結びつけるものです。このレイヤでは、状態の発生元とイベントの処理方法も定義できます。
以下は、典型的な ConversationScreen
です。他のモジュールからアクセスされないように、自身のモジュールに internal
を指定できます。
// ConversationScreen.kt
@Composable
internal fun ConversationScreen(
uiState: ConversationUiState,
onPinConversation: () -> Unit,
onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }
次の NavGraphBuilder
拡張関数は、その NavGraph
のデスティネーションとして ConversationScreen
コンポーザブルを追加するものです。また、画面 UI の状態を提供し、画面関連のビジネス ロジックを処理する ViewModel と画面を統合します。ViewModel で処理できないナビゲーション イベントは、呼び出し元に公開されます。
// 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 ライブラリのコードをデスティネーション自体から分離します。また、ルートや引数 ID など、Navigation のコンセプトに関するカプセル化も行います。ルートや引数 ID は非公開にして、このファイルの外部に漏洩しないようにする必要があります。このレイヤで処理できないナビゲーション イベントは、呼び出し元に公開して適切なレベルで処理されるようにする必要があります。上記のコード スニペットでは、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)
}
ナビゲーション グラフを作成する
ナビゲーション グラフは、上記の型安全な拡張関数を使用してデスティネーションを追加し、移動します。
次の例では、conversation デスティネーションと他の 2 つのデスティネーション(home と participant list)がアプリレベルの 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()
}
ネストされたナビゲーション グラフでの型安全性
複数の画面を提供するモジュールには、適切な公開設定を選択する必要があります。これは、上記セクションの各メソッドと同じコンセプトです。ただし、各画面を他のモジュールに個別に公開することはまったく意味がありません。個別に公開するのではなく、より大きな自己完結型フローの一部として処理します。
この自己完結型の画面セットを「ネストされたナビゲーション グラフ」と言います。これにより、複数の画面を 1 つの NavGraphBuilder
拡張メソッドに含められます。このメソッドは、NavController
拡張メソッドを順番に使用し、同じモジュール内の画面を 1 つに結びつけます。
次の例では、前のセクションで説明した conversation デスティネーションが、他の 2 つのデスティネーション(conversation list と participant list)とともにネストされたナビゲーション グラフに表示されます。
// 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)
}