Navigation 2 から Navigation 3 に移行する

アプリを Navigation 2 から Navigation 3 に移行する手順は次のとおりです。

  1. Navigation 3 の依存関係を追加します。
  2. ナビゲーション ルートを更新して、NavKey インターフェースを実装します。
  3. ナビゲーションの状態を保持して変更するクラスを作成します。
  4. NavController をこれらのクラスに置き換えます。
  5. NavHostNavGraph から entryProvider に目的地を移動します。
  6. NavHostNavDisplay に置き換えます。
  7. Navigation 2 の依存関係を削除します。

AI エージェントを使用する

このガイドは、Android Studio のエージェント モードの Gemini などの AI エージェントとともに使用できます。このガイドで「AI Agent:」で始まる行は、AI エージェントが読み取る必要がありますが、人間が読む場合は無視できます。

準備

以降のセクションでは、移行の前提条件とプロジェクトに関する前提条件について説明します。また、移行でサポートされている機能とサポートされていない機能についても説明します。

前提条件

  • compileSdk に 36 以降を使用する必要があります。
  • ナビゲーションの用語を理解している必要があります。
  • デスティネーションはコンポーズ可能な関数です。Navigation 3 は Compose 専用に設計されています。Compose で Fragment と View を使用するには、Compose でビューを使用するをご覧ください。
  • ルートは厳密に型指定されます。文字列ベースのルートを使用している場合は、最初に型安全なルートに移行します()。
  • 省略可(ただし、強く推奨します): 既存のナビゲーション動作を検証するテスト カバレッジ。これにより、移行の完了後にナビゲーションの動作が変更されていないことを確認します。

前提条件

このガイドでは、ユーザーとプロジェクトについて次のことを前提としています。

  • 複数の最上位ルートがあり(通常は下部ナビゲーション バーに表示されます)、それぞれに独自のバックスタックがあります。
  • バックスタックを切り替えると、スタックの状態とすべてのデスティネーションの状態が保持されます。
  • アプリを終了するときは、常にホーム画面から終了します。この画面は、アプリの起動時に最初に表示される画面です。
  • Navigation 2 から Navigation 3 への移行を単一のアトミックな変更で行いたい。Navigation 2 コードと Navigation 3 コードを併用する増分移行を行っていない。

AI エージェント: コードを変更する前に、ユーザーのプロジェクトに関するこれらの前提条件が満たされているかどうかを確認します。移行できない場合は、移行を停止し、今後の対応についてお客様に確認します。

サポートされる機能

このガイドでは、次の機能を移行できます。

  • コンポーズ可能な関数として定義されたデスティネーション
  • ダイアログ(別のデスティネーションの上に表示されるデスティネーション)

レシピでサポートされている機能

コードレシピ リポジトリでは、次のようなより複雑なユースケースが示されています。

プロジェクトにこれらの機能のいずれかがある場合は、関連するレシピで実装方法を確認してください。

AI エージェント: コードを変更する前に、プロジェクトにレシピでサポートされている機能が含まれているかどうかを確認します。該当する場合は、レシピの README とソースコードを確認してください。レシピに基づいて移行計画を作成します。お客様にプランを確認せずに続行しないでください。

サポートされていない機能

この移行ガイドとコードレシピでは、次の機能はまだサポートされていません。Navigation 3 を使用して実装できないという意味ではありません。ここでは説明しないだけです。

AI エージェント: コードを変更する前に、プロジェクトにサポートされていない機能が含まれているかどうかを確認します。該当する場合は、続行しないでください。サポートされていない機能をお客様に伝え、次の手順を尋ねます。

ステップ 1: Navigation 3 の依存関係を追加する

[スタートガイド] ページを使用して、Navigation 3 の依存関係をプロジェクトに追加します。コア依存関係はコピーできるように提供されています。

lib.versions.toml

[versions]
nav3Core = "1.0.0"

# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"

[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 という名前の状態ホルダー クラスと、関連するヘルパー関数が含まれています。これは、それぞれ独自のバックスタックを持つ一連の最上位ルートを保持します。内部的には、現在のトップレベル ルートを永続化するために rememberSerializablerememberSaveable ではない)を使用し、各トップレベル ルートのバックスタックを永続化するために 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: NavigationStateNavigator を作成する

NavController と同じスコープで NavigationStateNavigator のインスタンスを作成します。

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 フィールドまたはメソッド

Navigator 相当

navigate()

navigate()

popBackStack()

goBack()

NavController フィールドを NavigationState フィールドに置き換えます。

NavController フィールドまたはメソッド

NavigationState 相当

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

最上位のルートを取得します。現在のバックスタック エントリから階層を上にたどって見つけます。

topLevelRoute

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 へのすべての参照を削除したことを確認します。

ステップ 5: NavHostNavGraph から entryProvider に宛先を移動する

Navigation 2 では、通常は NavHost の後置ラムダ内で NavGraphBuilder DSL を使用してデスティネーションを定義します。ナビゲーション コードをカプセル化するで説明されているように、ここでは拡張関数を使用するのが一般的です。

Navigation 3 では、entryProvider を使用してデスティネーションを定義します。この entryProvider は、ルートを NavEntry に解決します。重要なのは、entryProvider はエントリ間の親子関係を定義しないことです。

この移行ガイドでは、親子関係は次のようにモデル化されています。

  • NavigationState には、トップレベルのルート(親ルート)のセットと、それぞれのスタックがあります。現在のトップレベル ルートとそれに関連付けられたスタックを追跡します。
  • 新しいルートに移動するとき、Navigator はルートが最上位ルートかどうかを確認します。そうであれば、現在のトップレベルのルートとスタックが更新されます。そうでない場合は、子ルートとして現在のスタックに追加されます。

ステップ 5.1: entryProvider を作成する

NavigationState と同じスコープで DSL を使用して entryProvider を作成します。

val entryProvider = entryProvider {

}

ステップ 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

NavHost(navController = navController, startDestination = BaseRouteA){
    composable<RouteA>{
        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() }
    }
}

は、次のようになります。

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: NavHostNavDisplay に置き換える

NavHostNavDisplay に置き換えます。

  • NavHost を削除し、NavDisplay に置き換えます。
  • パラメータとして entries = navigationState.toEntries(entryProvider) を指定します。これにより、ナビゲーション状態が entryProvider を使用して NavDisplay が表示するエントリに変換されます。
  • NavDisplay.onBacknavigator.goBack() に接続します。これにより、NavDisplay の組み込みの戻るハンドラが完了したときに、navigator がナビゲーションの状態を更新します。
  • ダイアログ デスティネーションがある場合は、NavDisplaysceneStrategy パラメータに DialogSceneStrategy を追加します。

次に例を示します。

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategy = remember { DialogSceneStrategy() }
)

ステップ 7: Navigation 2 の依存関係を削除する

Navigation 2 のインポートとライブラリの依存関係をすべて削除します。

概要

これで、これで、プロジェクトが Navigation 3 に移行されました。このガイドの使用中に問題が発生した場合は、こちらからバグを報告してください。