Navigation 3 では、シーンを使用してアプリの 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>>
と onBack
コールバックを受け取ります。指定されたエントリから正常に形成できる場合は Scene<T>
を返します。そうでない場合は null
を返します。
SceneStrategy
には、複数の戦略を連結できる便利な then
インフィックス関数も用意されています。これにより、各戦略が Scene
の計算を試行し、計算できない場合はチェーンの次の戦略に委任できる、柔軟な意思決定パイプラインが作成されます。
シーンとシーン戦略の連携の仕組み
NavDisplay
は、バックスタックを監視し、SceneStrategy
を使用して適切な Scene
を決定してレンダリングする中心的なコンポーザブルです。
NavDisplay's sceneStrategy
パラメータは、表示する Scene
の計算を行う SceneStrategy
を想定しています。指定された戦略(または戦略のチェーン)で Scene
が計算されない場合、NavDisplay
はデフォルトで SinglePaneSceneStrategy
を使用するように自動的にフォールバックします。
インタラクションの詳細は次のとおりです。
- バックスタックにキーを追加または削除すると(
backStack.add()
やbackStack.removeLastOrNull()
を使用するなど)、NavDisplay
はこれらの変更を監視します。 NavDisplay
は、現在のNavEntrys
リスト(バックスタックキーから派生)を、設定されたSceneStrategy's calculateScene
メソッドに渡します。SceneStrategy
がScene
を正常に返すと、NavDisplay
はそのScene
のcontent
をレンダリングします。NavDisplay
は、Scene
のプロパティに基づいてアニメーションと予測型「戻る」も管理します。
例: シングルペイン レイアウト(デフォルトの動作)
最もシンプルなカスタム レイアウトは、シングルペイン ディスプレイです。これは、他の SceneStrategy
が優先されない場合のデフォルトの動作です。
data class SinglePaneScene<T : Any>( override val key: T, 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.invoke(entry.key) } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { @Composable override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> = SinglePaneScene( key = entries.last().key, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
例: 基本的な 2 ペイン レイアウト(カスタム シーンと戦略)
この例では、次の 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.invoke(firstEntry.key) } Column(modifier = Modifier.weight(0.5f)) { secondEntry.content.invoke(secondEntry.key) } } } 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) } } // --- 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> : SceneStrategy<T> { @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class) @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass // 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.key, secondEntry.key) 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 } } }
この TwoPaneSceneStrategy
を NavDisplay
で使用するには、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 = TwoPaneSceneStrategy<Any>(), onBack = { count -> repeat(count) { 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<Any>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { keysToRemove -> repeat(keysToRemove) { 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") } } ) } } } }