基本を理解して実装する

Navigation は、ユーザーがアプリ内を移動する方法を説明します。ユーザーは通常、タップまたはクリックによって UI 要素を操作し、アプリはそれに応じて新しいコンテンツを表示します。ユーザーが前のコンテンツに戻りたい場合は、[戻る] 操作を使用するか、[戻る] ボタンをタップします。

ナビゲーション状態のモデル化

この動作をモデル化する便利な方法は、コンテンツのスタックを使用することです。ユーザーが新しいコンテンツに進むと、そのコンテンツがスタックの一番上にプッシュされます。ユーザーがそのコンテンツから戻ると、そのコンテンツはスタックからポップされ、前のコンテンツが表示されます。ナビゲーションの用語では、このスタックは通常、バックスタックと呼ばれます。これは、ユーザーが戻ることができるコンテンツを表しているためです。

ソフトウェア キーボードの操作ボタン(チェックマーク アイコン)が赤で囲まれている。
図 1. ユーザーのナビゲーション イベントに応じてバックスタックがどのように変化するかを示す図。

バックスタックを作成する

Navigation 3 では、バックスタックにコンテンツは含まれません。代わりに、キーと呼ばれるコンテンツへの参照が含まれています。キーは任意の型にできますが、通常はシンプルなシリアル化可能なデータクラスです。コンテンツではなく参照を使用すると、次のようなメリットがあります。

  • バックスタックにキーをプッシュすることで、簡単にナビゲーションできます。
  • キーがシリアル化可能である限り、バックスタックを永続ストレージに保存できるため、構成変更やプロセスの終了後も存続できます。これは重要なことです。ユーザーは、アプリを終了して後で戻ってきたときに、同じコンテンツが表示され、中断したところから再開できることを期待しているからです。詳細については、バックスタックを保存するをご覧ください。

Navigation 3 API の主なコンセプトは、デベロッパーがバックスタックを所有することです。ライブラリ:

  • バックスタックがスナップショット状態をバックアップした List<T> であることを想定しています。ここで、T はバックスタック keys のタイプです。Any を使用することも、より厳密に型指定された独自のキーを指定することもできます。「push」または「pop」という用語は、リストの末尾にアイテムを追加または削除する実装を意味します。
  • バックスタックを監視し、NavDisplay を使用してその状態を UI に反映します。

次の例は、キーとバックスタックを作成し、ユーザー ナビゲーション イベントに応じてバックスタックを変更する方法を示しています。

// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)

@Composable
fun MyApp() {

    // Create a back stack, specifying the key the app should start with
    val backStack = remember { mutableStateListOf<Any>(ProductList) }

    // Supply your back stack to a NavDisplay so it can reflect changes in the UI
    // ...more on this below...

    // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
    backStack.add(ProductDetail(id = "ABC"))

    // Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
    backStack.removeLastOrNull()
}

キーをコンテンツに解決する

Navigation 3 では、コンポーズ可能な関数を含むクラスである NavEntry を使用してコンテンツがモデル化されます。これは、デスティネーション(ユーザーが進むまたは戻ることができる単一のコンテンツ)を表します。

NavEntry には、コンテンツに関する情報であるメタデータを含めることもできます。このメタデータは、NavDisplay などのコンテナ オブジェクトによって読み取られ、NavEntry のコンテンツの表示方法を決定するのに役立ちます。たとえば、メタデータを使用して、特定の NavEntry のデフォルトのアニメーションをオーバーライドできます。NavEntry metadata は、String キーを Any 値にマッピングするもので、汎用性の高いデータ ストレージを提供します。

key を NavEntry に変換するには、エントリ プロバイダを作成します。これは、key を受け取り、その key の NavEntry を返す関数です。通常は、NavDisplay の作成時にラムダ パラメータとして定義されます。

エントリ プロバイダを作成するには、ラムダ関数を直接作成するか、entryProvider DSL を使用するかの 2 つの方法があります。

エントリ プロバイダ関数を直接作成する

通常、Entry Provider 関数は when ステートメントを使用して作成し、キーごとにブランチを作成します。

entryProvider = { key ->
    when (key) {
        is ProductList -> NavEntry(key) { Text("Product List") }
        is ProductDetail -> NavEntry(
            key,
            metadata = mapOf("extraDataKey" to "extraDataValue")
        ) { Text("Product ${key.id} ") }

        else -> {
            NavEntry(Unit) { Text(text = "Invalid Key: $it") }
        }
    }
}

entryProvider DSL を使用する

entryProvider DSL を使用すると、各キータイプに対してテストを行い、それぞれに NavEntry を作成する必要がなくなるため、ラムダ関数を簡素化できます。これには entryProvider ビルダー関数を使用します。また、キーが見つからない場合のデフォルトのフォールバック動作(エラーをスローする)も含まれています。

entryProvider = entryProvider {
    entry<ProductList> { Text("Product List") }
    entry<ProductDetail>(
        metadata = mapOf("extraDataKey" to "extraDataValue")
    ) { key -> Text("Product ${key.id} ") }
}

このスニペットでは、次の点に注意してください。

  • entry は、指定されたタイプとコンポーズ可能なコンテンツを持つ NavEntry を定義するために使用されます。
  • entry は、NavEntry.metadata を設定する metadata パラメータを受け取ります。

バックスタックを表示する

バックスタックは、アプリのナビゲーション状態を表します。バックスタックが変更されるたびに、アプリの UI に新しいバックスタックの状態が反映される必要があります。Navigation 3 では、NavDisplay がバックスタックを監視し、それに応じて UI を更新します。次のパラメータを使用して作成します。

  • バックスタック - SnapshotStateList<T> 型にする必要があります。ここで、T はバックスタック キーの型です。これは監視可能な List であるため、変更されると NavDisplay の再コンポーズがトリガーされます。
  • バックスタック内のキーを NavEntry オブジェクトに変換する entryProvider。
  • 必要に応じて、onBack パラメータにラムダを指定します。これは、ユーザーが戻るイベントをトリガーしたときに呼び出されます。

次の例は、NavDisplay を作成する方法を示しています。

data object Home
data class Product(val id: String)

@Composable
fun NavExample() {

    val backStack = remember { mutableStateListOf<Any>(Home) }

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = { key ->
            when (key) {
                is Home -> NavEntry(key) {
                    ContentGreen("Welcome to Nav3") {
                        Button(onClick = {
                            backStack.add(Product("123"))
                        }) {
                            Text("Click to navigate")
                        }
                    }
                }

                is Product -> NavEntry(key) {
                    ContentBlue("Product ${key.id} ")
                }

                else -> NavEntry(Unit) { Text("Unknown route") }
            }
        }
    )
}

デフォルトでは、NavDisplay は、シングルペイン レイアウトのバックスタックの最上位の NavEntry を表示します。次の動画は、このアプリの動作を示しています。

2 つのデスティネーションがある場合の `NavDisplay` のデフォルトの動作。
図 2. NavDisplay のデフォルトの動作(2 つの宛先)。

宛先のライフサイクル

NavDisplay は、カスタム LifecycleOwner を使用して、Scene レベルの制約と Entry レベルの制約の両方に基づいて NavEntry のライフサイクル状態を制限します。

Compose のライフサイクルの詳細については、Jetpack Compose のライフサイクルをご覧ください。

シーンレベルのライフサイクルの制約

NavDisplay は、アクティブな Scene のライフサイクルを管理します。シーンレベルの上限は次のように決定されます。

オーバーレイ以外のシーンの場合:

  • RESUMED: シーンの遷移が完了し、その上にアクティブなオーバーレイ シーンが表示されていない場合にのみ許可されます。
  • STARTED: 前後に移動する場合や、オーバーレイで覆われている場合など、シーンの遷移中は STARTED に制限されます。

ダイアログやボトムシートなどのオーバーレイ シーンの場合:

  • RESUMED: 最上位の現在アクティブなオーバーレイ シーンでのみ許可されます。
  • STARTED: 新しいオーバーレイで覆われている、基盤となるオーバーレイ シーンでは STARTED を上限とします。

エントリレベルのライフサイクル状態

ライブラリは、バックスタック内の存在に基づいて、個々の NavEntry の最大ライフサイクル状態を管理します。

  • RESUMED: エントリが現在のバックスタックに存在する場合、そのライフサイクルは RESUMED まで進むことができます(シーンレベルの上限が適用されます)。
  • CREATED: エントリがバックスタックに存在しない場合(ポップされたが、アニメーション化中に画面にレンダリングされている場合など)、ライブラリはライフサイクルを CREATED に厳密に制限します。この制限により、バックグラウンドまたは終了中のエントリは、終了遷移を完了する間、フローの収集や RESUMED 状態または STARTED 状態にバインドされたコルーチンの起動などのアクティブな処理の実行を停止します。

組み合わせ

たとえば、NavEntry の最終的なライフサイクル状態は次のように解決されます。

シナリオ シーンレベルの上限 エントリーレベルの上限 有効な上限
アクティブなエントリ、固定画面(遷移やオーバーレイなし) RESUMED RESUMED RESUMED
アクティブなエントリ、遷移中(移動中または移動元) STARTED RESUMED STARTED
アクティブなエントリ、オーバーレイで覆われている(ダイアログが開いているなど) STARTED RESUMED STARTED
ポップされたエントリ、アニメーションで消える STARTED または RESUMED CREATED CREATED

すべてを組み合わせる

次の図は、Navigation 3 のさまざまなオブジェクト間でデータがどのように流れるかを示しています。

Navigation 3 のさまざまなオブジェクト間でデータがどのように流れるかを示す図。
図 3. Navigation 3 のさまざまなオブジェクトを介したデータフローを示す図。
  1. ナビゲーション イベントによって変更が開始されます。キーは、ユーザーの操作に応じてバックスタックに追加または削除されます。

  2. バックスタックの状態が変化すると、コンテンツの取得がトリガーされます。NavDisplay(バックスタックをレンダリングするコンポーザブル)は、バックスタックを監視します。デフォルトの構成では、単一ペイン レイアウトで最上位のバックスタック エントリが表示されます。バックスタックの一番上のキーが変更されると、NavDisplay はこのキーを使用して、対応するコンテンツをエントリ プロバイダにリクエストします。

  3. エントリ プロバイダがコンテンツを提供する。エントリ プロバイダは、キーを NavEntry に解決する関数です。エントリ プロバイダは、NavDisplay からキーを受け取ると、関連付けられた NavEntry を提供します。これにはキーとコンテンツの両方が含まれます。

  4. コンテンツが表示される。NavDisplay は NavEntry を受信してコンテンツを表示します。