Navigation 3 では、Scene を通じてアプリの UI フローを管理するための強力で柔軟なシステムが導入されています。シーンを使用すると、高度にカスタマイズされたレイアウトを作成し、さまざまな画面サイズに対応させ、複雑なマルチペイン エクスペリエンスをシームレスに管理できます。
シーンについて
Navigation 3 では、Scene は 1 つ以上の NavEntry インスタンスをレンダリングする基本単位です。Scene は、バックスタックのコンテンツの表示を格納して管理できる、UI の個別の視覚状態またはセクションと考えることができます。
各 Scene インスタンスは、key と Scene 自体のクラスによって一意に識別されます。この一意の識別子は、Scene が変更されたときに最上位のアニメーションを駆動するため、非常に重要です。
Scene インターフェースには次のプロパティがあります。
- key: Any: この特定の- Sceneインスタンスの一意の識別子。このキーは- Sceneのクラスと組み合わされ、主にアニメーションの目的で一意性を確保します。
- entries: List<NavEntry<T>>:- Sceneが表示する- NavEntryオブジェクトのリストです。重要な点として、同じ- NavEntryがトランジション中に複数の- Scenesに表示される場合(共有要素のトランジションなど)、そのコンテンツは、それを表示している最新のターゲット- Sceneによってのみレンダリングされます。
- previousEntries: List<NavEntry<T>>: このプロパティは、現在の- Sceneから「戻る」アクションが発生した場合に生じる- NavEntryを定義します。これは、適切な予測戻る状態を計算するために不可欠です。これにより、- NavDisplayは、異なるクラスやキーを持つ Scene である可能性のある、正しい前の状態を予測して移行できます。
- content: @Composable () -> Unit:- Sceneが- entriesと、その- Sceneに固有の周囲の UI 要素をレンダリングする方法を定義するコンポーズ可能な関数です。
シーン戦略を理解する
SceneStrategy は、バックスタックの NavEntry のリストをどのように配置して Scene に移行するかを決定するメカニズムです。基本的に、現在のバックスタック エントリが提示されると、SceneStrategy は次の 2 つの重要な質問を自問します。
- これらのエントリから Sceneを作成できますか?SceneStrategyが、指定されたNavEntryを処理して意味のあるScene(ダイアログやマルチペイン レイアウトなど)を形成できると判断した場合、処理が続行されます。それ以外の場合はnullを返し、他の戦略がSceneを作成する機会を与えます。
- その場合、それらのエントリを Scene?にどのように配置すればよいですか?SceneStrategyがエントリの処理をコミットすると、Sceneの構築と、指定されたNavEntryがそのScene内にどのように表示されるかを定義する責任を負います。
SceneStrategy のコアは calculateScene メソッドです。
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
このメソッドは、バックスタックから現在の List<NavEntry<T>> を取得する SceneStrategyScope の拡張関数です。提供されたエントリから正常に作成できる場合は Scene<T> を、作成できない場合は null を返す必要があります。
SceneStrategyScope は、SceneStrategy が必要とする可能性のある任意の引数(onBack コールバックなど)を維持する役割を担います。
SceneStrategy には便利な then 中置関数も用意されており、複数の戦略を連結できます。これにより、各戦略が Scene の計算を試み、計算できない場合はチェーン内の次の戦略に委任できる、柔軟な意思決定パイプラインが作成されます。
シーンとシーン戦略の連携の仕組み
NavDisplay は、バックスタックを監視し、SceneStrategy を使用して適切な Scene を決定してレンダリングする中心的なコンポーザブルです。
NavDisplay's sceneStrategy パラメータは、表示する Scene の計算を担当する SceneStrategy を想定しています。指定された戦略(または戦略のチェーン)で Scene が計算されない場合、NavDisplay はデフォルトで SinglePaneSceneStrategy の使用に自動的にフォールバックします。
インタラクションの詳細は次のとおりです。
- バックスタックからキーを追加または削除すると(backStack.add()やbackStack.removeLastOrNull()を使用するなど)、NavDisplayはこれらの変更を監視します。
- NavDisplayは、構成された- SceneStrategy's calculateSceneメソッドに- NavEntrysの現在のリスト(バックスタックキーから派生)を渡します。
- SceneStrategyが- Sceneを正常に返すと、- NavDisplayはその- Sceneの- contentをレンダリングします。- 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 ペイン レイアウト(カスタムの Scene と戦略)
この例では、次の 2 つの条件に基づいてアクティブ化されるシンプルな 2 ペイン レイアウトを作成する方法を示します。
- ウィンドウの幅が、2 つのペインをサポートするのに十分な幅(WIDTH_DP_MEDIUM_LOWER_BOUND以上)である。
- バックスタックの上位 2 つのエントリは、特定のメタデータを使用して、2 ペイン レイアウトでの表示をサポートすることを明示的に宣言します。
次のスニペットは、TwoPaneScene.kt と TwoPaneSceneStrategy.kt の結合されたソースコードです。
// --- TwoPaneScene --- /** * A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split. */ class TwoPaneScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val firstEntry: NavEntry<T>, val secondEntry: NavEntry<T> ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(firstEntry, secondEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.5f)) { firstEntry.Content() } Column(modifier = Modifier.weight(0.5f)) { secondEntry.Content() } } } companion object { internal const val TWO_PANE_KEY = "TwoPane" /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * in a two-pane layout. */ fun twoPane() = mapOf(TWO_PANE_KEY to true) } } @Composable fun <T : Any> rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy<T> { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass return remember(windowSizeClass) { TwoPaneSceneStrategy(windowSizeClass) } } // --- TwoPaneSceneStrategy --- /** * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough * and the top two back stack entries declare support for two-pane display. */ class TwoPaneSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val lastTwoEntries = entries.takeLast(2) // Condition 2: Only return a Scene if there are two entries, and both have declared // they can be displayed in a two pane scene. return if (lastTwoEntries.size == 2 && lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } ) { val firstEntry = lastTwoEntries.first() val secondEntry = lastTwoEntries.last() // The scene key must uniquely represent the state of the scene. val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey) TwoPaneScene( key = sceneKey, // Where we go back to is a UX decision. In this case, we only remove the top // entry from the back stack, despite displaying two entries in this scene. // This is because in this app we only ever add one entry to the // back stack at a time. It would therefore be confusing to the user to add one // when navigating forward, but remove two when navigating back. previousEntries = entries.dropLast(1), firstEntry = firstEntry, secondEntry = secondEntry ) } else { null } } }
NavDisplay でこの TwoPaneSceneStrategy を使用するには、entryProvider 呼び出しを変更して、2 ペイン レイアウトで表示するエントリの TwoPaneScene.twoPane() メタデータを含めます。次に、sceneStrategy として TwoPaneSceneStrategy() を指定します。シングルペインのシナリオでは、デフォルトのフォールバックが使用されます。
// Define your navigation keys @Serializable data object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ProductList) NavDisplay( backStack = backStack, entryProvider = entryProvider { entry<ProductList>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Column { Text("Product List") Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View Details for ABC (Two-Pane Eligible)") } } } entry<ProductDetail>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Text("Product Detail: ${key.id} (Two-Pane Eligible)") } // ... other entries ... }, // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically. sceneStrategy = rememberTwoPaneSceneStrategy(), onBack = { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } ) }
マテリアル アダプティブ シーンでリスト詳細コンテンツを表示する
リストと詳細のユースケースでは、androidx.compose.material3.adaptive:adaptive-navigation3 アーティファクトは、リストと詳細の Scene を作成する ListDetailSceneStrategy を提供します。この Scene は、複雑なマルチペイン レイアウト(リスト、詳細、追加ペイン)を自動的に処理し、ウィンドウ サイズとデバイスの状態に基づいて調整します。
マテリアル リストの詳細 Scene を作成する手順は次のとおりです。
- 依存関係を追加する: プロジェクトの build.gradle.ktsファイルにandroidx.compose.material3.adaptive:adaptive-navigation3を含めます。
- ListDetailSceneStrategyメタデータを使用してエントリを定義する:- listPane(), detailPane()と- extraPane()を使用して、適切なペインに表示されるように- NavEntrysをマークします。- listPane()ヘルパーでは、項目が選択されていない場合に- detailPlaceholderを指定することもできます。
- rememberListDetailSceneStrategy() を使用する: このコンポーザブル関数は、- NavDisplayで使用できる事前構成済みの- ListDetailSceneStrategyを提供します。
次のスニペットは、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() }, sceneStrategy = 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") } } ) } } } }
