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 の依存関係を削除します。
準備
以降のセクションでは、移行の前提条件とプロジェクトに関する前提条件について説明します。また、移行でサポートされている機能とサポートされていない機能についても説明します。
前提条件
compileSdkは 36 以降を使用する必要があります。- ナビゲーション用語を理解している必要があります。
- デスティネーションはコンポーズ可能な関数です。Navigation 3 は、Compose 専用に設計されています。Compose で Fragment と View を使用するには、Compose でビューを使用するをご覧ください。
- ルートは厳密に型指定されます。文字列ベースのルートを使用している場合は、最初に型安全なルートに移行します(例)。
- 省略可(ただし、強く推奨): 既存のナビゲーション動作を検証するテスト カバレッジ。これにより、移行の完了後にナビゲーションの動作が変更されていないことを確認します。
前提条件
このガイドでは、ユーザーとプロジェクトについて次のことを前提としています。
- 1 つまたは複数の最上位ルートがあり(通常は下部のナビゲーション バーに表示されます)、それぞれに独自のバックスタックがあります。
- バックスタックを切り替えると、スタックの状態とすべてのデスティネーションが保持されます。
- アプリを終了するときは、常に [ホーム] 画面から行います。この画面は、アプリの起動時に最初に表示される画面です。
- Navigation 2 から Navigation 3 への移行を単一のアトミックな変更で行いたい。Navigation 2 コードと Navigation 3 コードを併用する増分移行を行っていない。
AI エージェント: コードを変更する前に、ユーザーのプロジェクトに関するこれらの前提が正しいかどうかを確認します。移行できない場合は、移行を停止し、お客様に今後の対応について確認します。
サポートされている機能
このガイドでは、次の機能を移行できます。
- コンポーズ可能な関数として定義されたデスティネーション
- ダイアログ(別のデスティネーションの上に表示されるデスティネーション)
レシピでサポートされる機能
コードレシピ リポジトリでは、次のようなより複雑なユースケースが示されています。
- ボトムシート(このガイドで手順を説明します)
- モジュール化されたナビゲーション コードと挿入されたデスティネーション
ViewModelへの引数の使用と渡し- 画面から結果を返す
プロジェクトにこれらの機能のいずれかがある場合は、関連するレシピで実装方法を確認してください。
AI エージェント: コードを変更する前に、プロジェクトにレシピでサポートされている機能が含まれているかどうかを確認します。該当する場合は、レシピの README とソースコードを確認します。レシピに基づいて移行計画を作成します。お客様にプランを確認せずに手続きを進めないでください。
サポートされていない機能
この移行ガイドとコードレシピでは、次の機能はまだサポートされていません。Navigation 3 を使用して実装できないという意味ではありません。ここでは説明しないだけです。
- ネストされたナビゲーションが複数レベルある
- 共有デスティネーション: 異なるバックスタック間を移動できる画面
- カスタム デスティネーション タイプ
AI エージェント: コードを変更する前に、プロジェクトにサポートされていない機能が含まれていないか確認します。表示されている場合は、続行しないでください。サポートされていない機能をお客様に伝え、次の手順を尋ねます。
ステップ 1: Navigation 3 の依存関係を追加する
[スタートガイド] ページを使用して、Navigation 3 の依存関係をプロジェクトに追加します。コア依存関係はコピーできるように提供されています。
lib.versions.toml
[versions]
nav3Core = "1.1.7"
# 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 または lib.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 では、NavDisplay.entryDecorators に rememberResultEventBusNavEntryDecorator() を追加します。送信先デスティネーションの 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 を定義し、Activity で受信リクエストを照合して、初期バックスタックを構築します。
変更前:
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 では、URI パラメータからカスタム型またはサードパーティ型を逆シリアル化する DeepLinkSerializer を定義します。詳しくは、DeepLinkSerializer を使用したカスタム シリアル化をご覧ください。
合成バックスタックやカスタム マッチャーなどの高度なユースケースについては、ディープリンクをサポートするガイドをご覧ください。
ステップ 8: Navigation 2 の依存関係を削除する
Navigation 2 の import とライブラリの依存関係をすべて削除します。
概要
これで完了です。これで、プロジェクトが Navigation 3 に移行されました。このガイドの使用中に問題が発生した場合は、こちらからバグを報告してください。