Android スキル
GitHub で表示Jetpack Navigation 3
android skills add navigation-3Navigation 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 への移行を 1 回のアトミックな変更で行うことを想定しています。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 は、各デスティネーションのコンポーズ可能なLifecycleOwner
コンテンツに、LocalLifecycleOwner.current を介してエントリ スコープの
を提供します。詳細については、デスティネーションのライフサイクルをご覧ください。
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()
ステップ 5: デスティネーションを NavHost の NavGraph から entryProvider に移動する
Navigation 2 では、デスティネーションを定義
NavGraphBuilder DSL を使用して、
通常、NavHost's の後置ラムダ内で定義します。ナビゲーション コードをカプセル化するで説明されているように、ここで拡張
関数を使用するのが一般的です。
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)を指定します。 これにより、ナビゲーションの状態が、NavDisplayが表示するエントリに変換されますentryProviderを使用して。 NavDisplay.onBackをnavigator.goBack()に接続します。これにより、NavDisplayの組み込みの戻るハンドラが完了すると、navigatorがナビゲーションの状態を更新します。- ダイアログ デスティネーションがある場合は、
DialogSceneStrategyをNavDisplayのsceneStrategiesパラメータに追加します。
次に例を示します。
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
ステップ 7: Navigation 2 の依存関係を削除する
Navigation 2 のインポートとライブラリの依存関係をすべて削除します。
概要
これで完了です。プロジェクトが Navigation 3 に移行されました。このガイドの使用中に問題が発生した場合は、こちら からバグを報告 してください。