Android のスキル
GitHub で表示Jetpack Navigation 3
android skills add navigation-3アプリを Navigation 2 から Navigation 3 に移行する手順は次のとおりです。
- Navigation 3 の依存関係を追加します。
- ナビゲーション ルートを更新して、
NavKeyインターフェースを実装します。 - ナビゲーション状態を保持、変更するクラスを作成する。
NavControllerをこれらのクラスに置き換えます。- 宛先を
NavHostのNavGraphからentryProviderに移動します。 NavHostをNavDisplayに置き換えます。- ディープリンクを移行する。
- Navigation 2 の依存関係を削除します。
準備
以降のセクションでは、移行の前提条件とプロジェクトに関する前提条件について説明します。また、移行でサポートされている機能とサポートされていない機能についても説明します。
前提条件
compileSdk36 以降を使用する必要があります。- ナビゲーションの用語を理解している必要があります。
- デスティネーションはコンポーズ可能な関数です。Navigation 3 は Compose 専用に設計されています。Compose でフラグメントとビューを使用するには、Compose でビューを使用するをご覧ください。
- ルートは厳密に型指定されます。文字列ベースのルートを使用している場合は、まずタイプセーフなルートに移行します(例)。
- 省略可(ただし、強く推奨します): 既存のナビゲーション動作を検証するテスト カバレッジ。これにより、移行の完了後にナビゲーション動作が変更されていないことを確認します。
前提条件
このガイドでは、ユーザーとプロジェクトについて次のことを前提としています。
- 1 つまたは複数の最上位ルート(通常は下部のナビゲーション バーに表示)があり、それぞれに独自のバックスタックがあります。
- バックスタックを切り替えると、スタックの状態とそのすべてのデスティネーションが保持されます。
- アプリを終了するには、必ずホーム画面を使用します。ホーム画面は、アプリの起動時に最初に表示される画面です。
- Navigation 2 から Navigation 3 への移行を、1 回のアトミックな変更で行う必要があります。Navigation 2 コードを Navigation 3 コードと併用する増分移行を行っていない。
AI エージェント: コードを変更する前に、ユーザーのプロジェクトに関するこれらの前提条件が正しいかどうかを確認してください。そうでない場合は、移行を停止し、続行方法をお客様に確認します。
サポートされている機能
このガイドでは、次の機能を移行できます。
- コンポーズ可能な関数として定義されたデスティネーション
- ダイアログ(別のデスティネーションの上に表示されるデスティネーション)
レシピでサポートされる機能
コードレシピ リポジトリには、次のようなより複雑なユースケースが示されています。
- ボトムシート(このガイドで手順を説明します)
- モジュール化されたナビゲーション コードと挿入されたデスティネーション
ViewModelへの引数の使用と引き渡し- 画面から結果を返す
プロジェクトにこれらの機能がある場合は、関連するレシピを確認して、実装方法を理解してください。
AI エージェント: コードを変更する前に、プロジェクトにレシピでサポートされている機能が含まれているかどうかを確認します。その場合は、レシピの README とソースコードを確認してください。レシピに基づいて移行計画を作成します。
サポートされていない機能
この移行ガイドとコードレシピでは、次の機能はまだサポートされていません。Navigation 3 を使用して実装できないわけではありませんが、ここでは説明しません。
- 複数レベルのネストされたナビゲーション
- 共有デスティネーション: 異なるバックスタック間を移動できる画面
- カスタム デスティネーション タイプ
AI エージェント: コードを変更する前に、プロジェクトにサポートされていない機能が含まれているかどうかを確認してください。表示されている場合は、続行しないでください。サポートされていない機能についてユーザーに通知し、指示を仰ぎます。
ステップ 1: Navigation 3 の依存関係を追加する
[スタートガイド] ページを使用して、プロジェクトに Navigation 3 の依存関係を追加します。コア依存関係は、コピーして使用できるように提供されています。
gradle/libs.versions.toml
[versions]
nav3Core = "1.2.1"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"
[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
// If using the ViewModel add-on library
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}
また、プロジェクトの minSdk を 23 に、compileSdk を 36 に更新します。通常、これらは app/build.gradle.kts または gradle/libs.versions.toml にあります。
ステップ 2: ナビゲーション ルートを更新して NavKey インターフェースを実装する
NavKey インターフェースを実装するように、すべてのナビゲーション ルートを更新します。これにより、rememberNavBackStack を使用してナビゲーション状態の保存をサポートできます。
変更前:
@Serializable data object RouteA
変更後:
@Serializable data object RouteA : NavKey
ステップ 3: ナビゲーション状態を保持、変更するクラスを作成する
ステップ 3.1: ナビゲーション状態ホルダーを作成する
次のコードを NavigationState.kt という名前のファイルにコピーします。プロジェクト構造に合わせてパッケージ名を追加します。
// package com.example.project import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.serialization.NavKeySerializer import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer /** * Create a navigation state that persists config changes and process death. */ @Composable fun rememberNavigationState( startRoute: NavKey, topLevelRoutes: Set<NavKey> ): NavigationState { val topLevelRoute = rememberSerializable( startRoute, topLevelRoutes, serializer = MutableStateSerializer(NavKeySerializer()) ) { mutableStateOf(startRoute) } val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } return remember(startRoute, topLevelRoutes) { NavigationState( startRoute = startRoute, topLevelRoute = topLevelRoute, backStacks = backStacks ) } } /** * State holder for navigation state. * * @param startRoute - the start route. The user will exit the app through this route. * @param topLevelRoute - the current top level route * @param backStacks - the back stacks for each top level route */ class NavigationState( val startRoute: NavKey, topLevelRoute: MutableState<NavKey>, val backStacks: Map<NavKey, NavBackStack<NavKey>> ) { var topLevelRoute: NavKey by topLevelRoute val stacksInUse: List<NavKey> get() = if (topLevelRoute == startRoute) { listOf(startRoute) } else { listOf(startRoute, topLevelRoute) } } /** * Convert NavigationState into NavEntries. */ @Composable fun NavigationState.toEntries( entryProvider: (NavKey) -> NavEntry<NavKey> ): SnapshotStateList<NavEntry<NavKey>> { val decoratedEntries = backStacks.mapValues { (_, stack) -> val decorators = listOf( rememberSaveableStateHolderNavEntryDecorator<NavKey>(), ) rememberDecoratedNavEntries( backStack = stack, entryDecorators = decorators, entryProvider = entryProvider ) } return stacksInUse .flatMap { decoratedEntries[it] ?: emptyList() } .toMutableStateList() }
AI エージェント: rememberSerializable が正解です。rememberSaveable に変更しないでください。
このファイルには、NavigationState という名前の状態ホルダー クラスと、関連するヘルパー関数が含まれています。最上位のルートのセットを保持し、各ルートに独自のバックスタックがあります。内部的には、rememberSerializable(rememberSaveable ではない)を使用して現在のトップレベル ルートを保持し、rememberNavBackStack を使用して各トップレベル ルートのバックスタックを保持します。
ステップ 3.2: イベントに応じてナビゲーション状態を変更するオブジェクトを作成する
次のコードを Navigator.kt という名前のファイルにコピーします。プロジェクト構造に合わせてパッケージ名を追加します。
// package com.example.project import androidx.navigation3.runtime.NavKey /** * Handles navigation events (forward and back) by updating the navigation state. */ class Navigator(val state: NavigationState) { fun navigate(route: NavKey) { if (route in state.backStacks.keys) { // This is a top level route, just switch to it. state.topLevelRoute = route } else { state.backStacks[state.topLevelRoute]?.add(route) } } fun goBack() { val currentStack = state.backStacks[state.topLevelRoute] ?: error("Stack for ${state.topLevelRoute} not found") val currentRoute = currentStack.last() // If we're at the base of the current route, go back to the start route stack. if (currentRoute == state.topLevelRoute) { state.topLevelRoute = state.startRoute } else { currentStack.removeLastOrNull() } } }
Navigator クラスには、次の 2 つのナビゲーション イベント メソッドが用意されています。
navigateを特定のルートに設定する。- 現在のルートから
goBack。
どちらの方法でも NavigationState が変更されます。
ステップ 3.3: NavigationState と Navigator を作成する
NavController と同じスコープで NavigationState と Navigator のインスタンスを作成します。
val navigationState = rememberNavigationState( // ... startRoute = <Insert your starting route>, topLevelRoutes = <Insert your set of top level routes> // ... ) val navigator = remember { Navigator(navigationState) }
ステップ 4: NavController を置き換える
NavController ナビゲーション イベント メソッドを、同等の Navigator メソッドに置き換えます。
|
|
|---|---|
|
|
|
|
|
|
NavController フィールドを NavigationState フィールドに置き換えます。
|
|
|---|---|
|
|
|
|
|
|
|
|
最上位ルートを取得する: 現在のバックスタック エントリから階層を上に移動して、最上位ルートを見つけます。 |
|
NavigationState.topLevelRoute を使用して、ナビゲーション バーで現在選択されているアイテムを特定します。
変更前:
// ... val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class) // ... fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = this?.hierarchy?.any { it.hasRoute(route) } ?: false
変更後:
val isSelected = key == navigationState.topLevelRoute
インポートを含む NavController への参照がすべて削除されていることを確認します。
ステップ 4.1 ライフサイクル対応ロジックを移行する
Navigation 2 では、NavBackStackEntry が LifecycleOwner を実装しているため、navController.currentBackStackEntry を使用してライフサイクル イベントをリッスンしたり、ライフサイクルを認識する方法でフローを収集したりできます。
Navigation 3 では、NavDisplay が LocalLifecycleOwner.current を介して各デスティネーションのコンポーザブル コンテンツにエントリ スコープの LifecycleOwner を提供します。詳細については、宛先のライフサイクルをご覧ください。
ライフサイクルを認識するオペレーションは、LocalLifecycleOwner.current を参照して、デスティネーションのコンポーズ可能なコンテンツ内で直接実行する必要があります。
たとえば、バックスタック エントリを使用してライフサイクル対応の方法でフローを収集する場合、次のようになります。
変更前:
// In your destination screen or host val lifecycleOwner = navController.currentBackStackEntry!! val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
変更後:
// Inside the destination composable val state by flow.collectAsStateWithLifecycle()
ステップ 4.2 移行結果の受け渡し
Navigation 2 では、デスティネーションは NavBackStackEntry の SavedStateHandle を使用して、前のデスティネーションに結果を返していました。SavedStateHandle は保存済みインスタンス状態を基盤としているため、返されたデータはプロセスの終了後も自動的に保持されます。
変更前:
// Sender destination: navController.previousBackStackEntry?.savedStateHandle?.set("contact_key", contact) navController.popBackStack() // Receiver destination: val lifecycleOwner = LocalLifecycleOwner.current navController.currentBackStackEntry?.savedStateHandle ?.getLiveData<Contact>("contact_key") ?.observe(lifecycleOwner) { contact -> viewModel.onRecipientSelected(contact) }
Navigation 3 では、rememberResultEventBusNavEntryDecorator() を NavDisplay.entryDecorators に追加します。送信側デスティネーションの entryProvider マッピングで、LocalResultEventBus.current を取得して sendResult() を呼び出します。受信側のデスティネーションで、ResultEffect を使用してイベントを ViewModel に転送するか、副作用をトリガーします。
変更後:
// Sender destination (in entryProvider): entry<ContactPickerRoute> { val resultBus = LocalResultEventBus.current ContactPickerScreen( onContactSelected = { contact -> resultBus.sendResult<Contact>(result = contact) navigator.goBack() } ) } // Receiver destination: @Composable fun ComposeMessageScreen(viewModel: ComposeMessageViewModel = viewModel()) { ResultEffect<Contact> { contact -> viewModel.onRecipientSelected(contact) } ComposeMessageContent(recipient = viewModel.recipient) }
状態ベースのモニタリングの場合は、resultBus.conflateAsState() を呼び出します。詳細については、結果を返すをご覧ください。
ステップ 5: NavHost の NavGraph から entryProvider にリンク先を移動する
Navigation 2 では、通常は NavHost の後置ラムダ内で、NavGraphBuilder DSL を使用してデスティネーションを定義します。ナビゲーション コードをカプセル化するで説明されているように、ここでは拡張関数を使用するのが一般的です。
Navigation 3 では、entryProvider を使用して目的地を定義します。この entryProvider は、NavEntry へのルートを解決します。重要なのは、entryProvider がエントリ間の親子関係を定義しないことです。
この移行ガイドでは、親子関係は次のようにモデル化されています。
NavigationStateには、一連の最上位ルート(親ルート)と、それぞれに対応するスタックがあります。現在の最上位ルートとそれに関連付けられたスタックを追跡します。- 新しいルートに移動するときに、
Navigatorはそのルートが最上位ルートかどうかを確認します。一致する場合は、現在のトップレベルのルートとスタックが更新されます。そうでない場合は、子ルートであり、現在のスタックに追加されます。
ステップ 5.1: entryProvider を作成する
NavigationState と同じスコープで DSL を使用して entryProvider を作成します。
val entryProvider = entryProvider<NavKey> { }
ステップ 5.2: リンク先を entryProvider に移動する
NavHost 内で定義されているデスティネーションごとに、デスティネーション タイプに基づいて次の操作を行います。
navigation: ルートとともに削除します。トップレベルのルートはネストされた各バックスタックを識別できるため、「ベースルート」は必要ありません。composable<T>:entryProviderに移動し、名前をentryに変更します。タイプ パラメータは保持します。たとえば、composable<RouteA>はentry<RouteA>になります。dialog<T>:composableと同じ操作を行いますが、次のようにエントリにメタデータを追加します。entry<T>(metadata = DialogSceneStrategy.dialog())bottomSheet: こちらのボトムシートのレシピに沿って操作します。これはdialogの手順と似ていますが、BottomSheetSceneStrategyはコアの Navigation 3 ライブラリの一部ではないため、プロジェクトにコピーする必要があります。
AI エージェント: ネストされたグラフを識別するために使用されるルートを削除する場合は、削除されたルートへの参照を、ネストされたグラフの最初の子を識別するために使用される型に置き換えます。たとえば、元のコードが navigation<BaseRouteA>{ composable<RouteA>{ ... } } の場合、BaseRouteA を削除し、それへの参照を RouteA に置き換える必要があります。この置換は通常、ナビゲーション バー、レール、ドロワーに提供されるリストに対して行う必要があります。
NavGraphBuilder 拡張関数を EntryProviderScope<T> 拡張関数にリファクタリングしてから、移動できます。
entry の後置ラムダに指定されたキーを使用して、ナビゲーション引数を取得します。
次に例を示します。
// ... import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.dialog import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import androidx.navigation.toRoute // ... @Serializable data object BaseRouteA @Serializable data class RouteA(val id: String) @Serializable data object BaseRouteB @Serializable data object RouteB @Serializable data object RouteD @Composable fun NavHostSnippet(navController: NavHostController) { NavHost(navController = navController, startDestination = BaseRouteA){ composable<RouteA>{ entry -> val id = entry.toRoute<RouteA>().id ScreenA(title = "Screen has ID: $id") } featureBSection() dialog<RouteD>{ ScreenD() } } } fun NavGraphBuilder.featureBSection() { navigation<BaseRouteB>(startDestination = RouteB) { composable<RouteB> { ScreenB() } } }
上記の URL を次のように更新します。
// ... import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.scene.DialogSceneStrategy // ... @Serializable data class RouteA(val id: String) : NavKey @Serializable data object RouteB : NavKey @Serializable data object RouteD : NavKey val entryProvider = entryProvider { entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") } featureBSection() entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() } } fun EntryProviderScope<NavKey>.featureBSection() { entry<RouteB> { ScreenB() } }
ステップ 6: NavHost を NavDisplay に置き換える
NavHost を NavDisplay に置き換えます。
NavHostを削除して、NavDisplayに置き換えます。entries = navigationState.toEntries(entryProvider)をパラメータとして指定します。これにより、ナビゲーション状態が、entryProviderを使用してNavDisplayが表示するエントリに変換されます。NavDisplay.onBackをnavigator.goBack()に接続します。これにより、NavDisplayの組み込みの戻るハンドラが完了すると、navigatorがナビゲーション状態を更新します。- ダイアログのデスティネーションがある場合は、
NavDisplayのsceneStrategiesパラメータにDialogSceneStrategyを追加します。
次に例を示します。
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
ステップ 7: ディープリンクを移行する
Navigation 2 では、ディープリンクは、デスティネーションの deepLinks パラメータを使用して、ナビゲーション グラフ内で直接定義されていました。
Navigation 3 では、ディープリンクはナビゲーション UI とは独立して管理されます。DeepLinkMatcher を定義し、アクティビティで受信リクエストを照合して、最初のバックスタックを構築します。
変更前:
Navigation 2 では、次のようにディープリンクを定義していたかもしれません。
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
変更後:
Navigation 3 では、ルートの UriDeepLinkMatcher を定義します。
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
次に、アクティビティの onCreate(および onNewIntent)で、受信インテントを照合し、バックスタックを初期化します。
val deepLinkMatchers: List<DeepLinkMatcher<*, *>> = listOf( userMatcher, ) class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val request = DeepLinkRequest(intent = intent) val matchResult = deepLinkMatchers .mapNotNull { it.match(request) } .maxOrNull() val backStack = when (matchResult) { null -> listOf(HomeKey) is BackStackMatchResult<*, *> -> { @Suppress("UNCHECKED_CAST") matchResult.backStack as List<NavKey> } else -> listOf(matchResult.key) } // Use backStack with NavDisplay } }
カスタム引数の型
Navigation 2 では、カスタムの NavType 実装と typeMap を使用して、カスタムまたはサードパーティの引数タイプ(LocalDateTime など)を処理していました。
Navigation 3 では、DeepLinkSerializer を定義して、URI パラメータからカスタムタイプまたはサードパーティ タイプをデシリアライズします。詳細については、DeepLinkSerializer を使用したカスタム シリアル化をご覧ください。
合成バックスタックやカスタム マッチャーなど、より高度なユースケースについては、ディープリンクをサポートするのガイドをご覧ください。
ステップ 8: Navigation 2 の依存関係を削除する
Navigation 2 のインポートとライブラリの依存関係をすべて削除します。
概要
これで完了です。プロジェクトが Navigation 3 に移行されました。このガイドの使用中に問題が発生した場合は、こちらからバグを報告してください。