シーンを使用してカスタム レイアウトを作成する

Navigation 3 では、シーン を使用してアプリの UI フローを管理するための強力で柔軟なシステムが導入されています。シーンを使用すると、高度にカスタマイズされたレイアウトを作成し、さまざまな画面サイズに対応させ、複雑なマルチペイン エクスペリエンスをシームレスに管理できます。

シーンについて

Navigation 3 では、Scene は 1 つ以上の NavEntry インスタンスをレンダリングする基本単位です。Scene は、バックスタックからコンテンツの表示を含めて管理できる、UI の個別のビジュアル状態またはセクションと考えることができます。

Scene インスタンスは、その keyScene 自体のクラスによって一意に識別されます。この一意の識別子は、 最上位のアニメーションを駆動するため、非常に重要です。Scene が変更されたときに

Scene インターフェースには次のプロパティがあります。

  • key: Any: この特定の Scene インスタンスの一意の識別子。このキーは、Scene のクラスと組み合わせて、主にアニメーションの目的で一意性を確保します。
  • entries: List<NavEntry<T>>: これは、NavEntry オブジェクトのリストです。 Scene が表示を担当します。重要な点として、トランジション中に同じ NavEntry が複数の Scenes に表示される場合(共有 要素のトランジションなど)、そのコンテンツは、それを表示している最新の ターゲット Scene によってのみレンダリングされます。
  • previousEntries: List<NavEntry<T>>: このプロパティは、現在の Sceneから「戻る」操作が行われた場合に生成される NavEntryを定義します。適切な予測型「戻る」状態を計算し、NavDisplay が正しい前の状態(クラスやキーが異なる Scene の場合もある)を予測してトランジションできるようにするために不可欠です。
  • content: @Composable () -> Unit: これは、 がその Scene と、その Scene に固有の周囲の UI 要素をどのようにレンダリングするかを定義するコンポーズ可能な関数です。entries
  • metadata: Map<String, Any>: などの他のライブラリ コンポーネントにシーン固有の情報を提供します。NavDisplayデフォルトでは、entries の最後の NavEntrymetadata を返します。

シーン戦略について

SceneStrategy は、バックスタックから取得した NavEntry のリストをどのように配置して Scene にトランジションさせるかを決定するメカニズムです。基本的には、現在のバックスタック エントリが提示されると、SceneStrategy は次の 2 つの重要な質問を自問します。

  1. これらのエントリから Scene を作成できますか?SceneStrategy が、指定された NavEntry を処理して意味のある Scene (ダイアログやマルチペイン レイアウトなど)を形成できると判断した場合は、処理を続行します。それ以外の場合は null を返し、他の戦略が Scene を作成する機会を与えます。
  2. 作成できる場合、それらのエントリを Scene? にどのように配置すればよいですか? SceneStrategy がエントリの処理をコミットすると、 の構築と、指定された NavEntry をその Scene 内にどのように表示するかを定義する責任を負います。Scene

SceneStrategy のコアは calculateScene メソッドです。

@Composable
public fun calculateScene(
    entries: List<NavEntry<T>>,
    onBack: (count: Int) -> Unit,
): Scene<T>?

このメソッドは、バックスタックから現在の List<NavEntry<T>> を取得する SceneStrategyScope の拡張関数です。指定されたエントリから正常に作成できる場合は Scene<T> を返し、作成できない場合は null if it を返す必要があります。

SceneStrategyScope は、SceneStrategy が必要とする可能性のあるオプションの引数 (onBack コールバックなど)を維持する役割を担います。

シーンとシーン戦略の連携

NavDisplay は、バックスタックを監視し、 1 つ以上の SceneStrategy を使用して適切な Scene を決定してレンダリングする中央のコンポーザブルです。

NavDisplay's sceneStrategies パラメータには、表示する Scene の計算を担当する SceneStrategy インスタンスのリストが必要です。指定された戦略で Sceneが計算されない場合、NavDisplayはデフォルトで を自動的にフォールバックして使用します。SinglePaneSceneStrategy

インタラクションの内訳は次のとおりです。

  • バックスタックからキーを追加または削除すると(backStack.add()backStack.removeLastOrNull() を使用するなど)、NavDisplay はこれらの変更を監視します。
  • NavDisplay は、現在の NavEntry のリスト(バック スタック キーから派生)を構成済みの sceneStrategies に順番に渡し、 calculateScene が返されるまで各 Scene を呼び出します。
  • SceneStrategyScene を正常に返すと、NavDisplay はその後 Scenecontent をレンダリングします。NavDisplay は、Scene のプロパティに基づいて、アニメーションと予測型「戻る」も管理します。

例: シングルペイン レイアウト(デフォルトの動作)

最もシンプルなカスタム レイアウトはシングルペイン表示です。これは、他の SceneStrategy が優先されない場合のデフォルトの動作です。

data class SinglePaneScene<T : Any>(
    override val key: Any,
    val entry: NavEntry<T>,
    override val previousEntries: List<NavEntry<T>>,
) : Scene<T> {
    override val entries: List<NavEntry<T>> = listOf(entry)
    override val content: @Composable () -> Unit = { entry.Content() }
}

/**
 * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the
 * list.
 */
public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> {
    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? =
        SinglePaneScene(
            key = entries.last().contentKey,
            entry = entries.last(),
            previousEntries = entries.dropLast(1)
        )
}

例: 基本的なリストと詳細のレイアウト(シーンと戦略のカスタム)

この例では、次の 2 つの条件に基づいてアクティブ化されるシンプルなリストと詳細のレイアウトを作成する方法を示します。

  1. ウィンドウの幅 が 2 つのペインをサポートするのに十分な幅である(WIDTH_DP_MEDIUM_LOWER_BOUND 以上)。
  2. バックスタックに、特定のメタデータを使用してリストと詳細のレイアウトでの表示のサポートを宣言したエントリが含まれている。

次のスニペットは ListDetailScene.kt のソースコードで、 ListDetailSceneListDetailSceneStrategy の両方が含まれています。

// --- ListDetailScene ---
/**
 * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split.
 *
 */
class ListDetailScene<T : Any>(
    override val key: Any,
    override val previousEntries: List<NavEntry<T>>,
    val listEntry: NavEntry<T>,
    val detailEntry: NavEntry<T>,
) : Scene<T> {
    override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry)
    override val content: @Composable (() -> Unit) = {
        Row(modifier = Modifier.fillMaxSize()) {
            Column(modifier = Modifier.weight(0.4f)) {
                listEntry.Content()
            }
            Column(modifier = Modifier.weight(0.6f)) {
                detailEntry.Content()
            }
        }
    }
}

@Composable
fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass

    return remember(windowSizeClass) {
        ListDetailSceneStrategy(windowSizeClass)
    }
}

// --- ListDetailSceneStrategy ---
/**
 * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item
 * is the backstack is a detail, and before it, at any point in the backstack is a list.
 */
class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> {

    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? {

        if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
            return null
        }

        val detailEntry =
            entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null
        val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null

        // We use the list's contentKey to uniquely identify the scene.
        // This allows the detail panes to be displayed instantly through recomposition, rather than
        // having NavDisplay animate the whole scene out when the selected detail item changes.
        val sceneKey = listEntry.contentKey

        return ListDetailScene(
            key = sceneKey,
            previousEntries = entries.dropLast(1),
            listEntry = listEntry,
            detailEntry = detailEntry
        )
    }

    object ListKey : NavMetadataKey<Boolean>
    object DetailKey : NavMetadataKey<Boolean>
    companion object {

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun listPane() = metadata {
            put(ListKey, true)
        }

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun detailPane() = metadata {
            put(DetailKey, true)
        }
    }
}

NavDisplay でこの ListDetailSceneStrategy を使用するには、entryProvider 呼び出しを変更して、リスト レイアウトとして表示するエントリの ListDetailScene.listPane() メタデータと、詳細 レイアウトとして表示するエントリの ListDetailScene.detailPane() を含めます。次に、ListDetailSceneStrategy()sceneStrategy として指定し、シングルペイン シナリオのデフォルトの代替に依存します。

// Define your navigation keys
@Serializable
data object ConversationList : NavKey

@Serializable
data class ConversationDetail(val id: String) : NavKey

@Composable
fun MyAppContent() {
    val backStack = rememberNavBackStack(ConversationList)
    val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        sceneStrategies = listOf(listDetailStrategy),
        entryProvider = entryProvider {
            entry<ConversationList>(
                metadata = ListDetailSceneStrategy.listPane()
            ) {
                Column(modifier = Modifier.fillMaxSize()) {
                    Text(text = "I'm a Conversation List")
                    Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) {
                        Text(text = "Open detail")
                    }
                }
            }
            entry<ConversationDetail>(
                metadata = ListDetailSceneStrategy.detailPane()
            ) {
                Text(text = "I'm a Conversation Detail")
            }
        }
    )
}

private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) {

    // Remove any existing detail routes, then add the new detail route
    removeIf { it is ConversationDetail }
    add(detailRoute)
}

独自のリストと詳細のシーンを作成したくない場合は、次のセクションで示すように、適切な詳細とプレースホルダのサポートを備えたマテリアル リストと詳細のシーンを使用できます。

マテリアル アダプティブ シーンにリストと詳細のコンテンツを表示する

**リストと詳細のユースケース** では、 androidx.compose.material3.adaptive:adaptive-navigation3 アーティファクトは、 ListDetailSceneStrategy を作成するリストと詳細の Scene を提供します。この Scene は、複雑なマルチペイン配置(リスト、詳細、追加のペイン)を自動的に処理し、ウィンドウ サイズとデバイスの状態に基づいて調整します。

マテリアル リストと詳細の Scene を作成する手順は次のとおりです。

  1. 依存関係を追加する: プロジェクトの androidx.compose.material3.adaptive:adaptive-navigation3 ファイルに build.gradle.kts を含めます。
  2. メタデータを使用してエントリを定義する: listPane(), detailPane() および extraPane() を使用して、適切なペイン表示用の NavEntrys をマークします。ListDetailSceneStrategylistPane() ヘルパーを使用すると、アイテムが選択されていない場合に detailPlaceholder を指定することもできます。
  3. rememberListDetailSceneStrategy を使用する : このコンポーズ可能な関数は、ListDetailSceneStrategy で使用できる事前構成済みの NavDisplay を提供します。

次のスニペットは、ListDetailSceneStrategy の使用方法を示す Activity のサンプルです。

@Serializable
object ProductList : NavKey

@Serializable
data class ProductDetail(val id: String) : NavKey

@Serializable
data object Profile : NavKey

class MaterialListDetailActivity : ComponentActivity() {

    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Scaffold { paddingValues ->
                val backStack = rememberNavBackStack(ProductList)
                val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()

                NavDisplay(
                    backStack = backStack,
                    modifier = Modifier.padding(paddingValues),
                    onBack = { backStack.removeLastOrNull() },
                    sceneStrategies = listOf(listDetailStrategy),
                    entryProvider = entryProvider {
                        entry<ProductList>(
                            metadata = ListDetailSceneStrategy.listPane(
                                detailPlaceholder = {
                                    ContentYellow("Choose a product from the list")
                                }
                            )
                        ) {
                            ContentRed("Welcome to Nav3") {
                                Button(onClick = {
                                    backStack.add(ProductDetail("ABC"))
                                }) {
                                    Text("View product")
                                }
                            }
                        }
                        entry<ProductDetail>(
                            metadata = ListDetailSceneStrategy.detailPane()
                        ) { product ->
                            ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) {
                                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                                    Button(onClick = {
                                        backStack.add(Profile)
                                    }) {
                                        Text("View profile")
                                    }
                                }
                            }
                        }
                        entry<Profile>(
                            metadata = ListDetailSceneStrategy.extraPane()
                        ) {
                            ContentGreen("Profile")
                        }
                    }
                )
            }
        }
    }
}

図 1 : マテリアル リストと詳細のシーンで実行されているコンテンツの例。