Navigation 2 から Navigation 3 に移行する

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

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

準備

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

前提条件

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

前提条件

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

  • 1 つ以上のトップレベル ルート(通常はボトム ナビゲーション バーに表示)があり、それぞれに独自のバックスタックがあります。
  • バックスタックを切り替えても、スタックの状態とすべてのデスティネーションが保持されます。
  • アプリを終了するときは、常に [ホーム] 画面から終了します。これは、アプリの起動時に最初に表示される画面です。
  • Navigation 2 から Navigation 3 への移行を 1 回のアトミックな変更で行うことを想定しています。Navigation 2 のコードと Navigation 3 のコードを併用する増分移行は行いません。

AI エージェント: コードを変更する前に、 ユーザーのプロジェクトに関するこれらの前提条件が満たされているかどうかを確認してください。満たされていない場合は、移行を停止し、ユーザーに続行方法を尋ねてください。

サポートされている機能

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

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

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

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

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

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 という名前の状態ホルダー クラスと関連するヘルパー関数が含まれています。これには、それぞれ独自のバックスタックを持つトップレベル ルートのセットが含まれています。内部的には、現在のトップレベル ルートを保持するために 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 への参照をすべて削除したことを確認します。

ステップ 4.1: ライフサイクル対応ロジックを移行する

Navigation 2 では、NavBackStackEntryLifecycleOwner を実装しているため、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: デスティネーションを NavHostNavGraph から 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: NavHostNavDisplay に置き換える

NavHostNavDisplay に置き換えます。

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

次に例を示します。

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

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

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

概要

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