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 要素をレンダリングする方法を定義するコンポーズ可能な関数です。metadata: Map<String, Any>:NavDisplayなどの他のライブラリ コンポーネントにシーン固有の情報を提供します。デフォルトでは、entriesの最後のNavEntryのmetadataを返します。
カスタム Scene に equals と hashCode を実装する
カスタム Scene 実装は、以下をサポートするために equals と hashCode を正しく実装する必要があります。
- シーンの状態遷移: シーン戦略のリストによってシーンが計算されるたびに、
NavDisplayは新しいシーンと前のシーンが等しいかどうかを確認します。変更が検出されると、シーン間の遷移に使用されるSeekableTransitionが更新され、シーンのライフサイクル状態に影響します。 - オーバーレイ管理:
OverlayScene(ダイアログなど)の場合、NavDisplayはシーン オブジェクト自体をkeyとして使用し、オーバーレイのライフサイクルと終了アニメーションを追跡します。
シーンのコンテンツを定義するすべてのプロパティ(key、entries、previousEntries など)が、equals と hashCode の実装に含まれていることを確認します。一般に、これらのメソッドの実装にコールバック(onBack など)を含めることは避けてください。論理シーンの状態を変更せずにインスタンスを変更する可能性があるためです。
シーン戦略を理解する
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> を正常に形成できる場合は Scene<T> を返し、形成できない場合は null を返す必要があります。
SceneStrategyScope は、SceneStrategy が必要とするオプションの引数(onBack コールバックなど)を維持する役割を担います。
シーンとシーン戦略の連携の仕組み
NavDisplay は、バックスタックを監視し、1 つ以上の SceneStrategy を使用して適切な Scene を決定してレンダリングする、中心的なコンポーザブルです。
NavDisplay の sceneStrategies パラメータは、表示する Scene の計算を担当する SceneStrategy インスタンスのリストを想定しています。指定された戦略で Scene が計算されない場合、NavDisplay はデフォルトで SinglePaneSceneStrategy の使用に自動的にフォールバックします。
やり取りの詳細は次のとおりです。
- バックスタックにキーを追加または削除すると(
backStack.add()やbackStack.removeLastOrNull()を使用する場合など)、NavDisplayはこれらの変更を監視します。 NavDisplayは、NavEntryの現在のリスト(バックスタック キーから派生)を、構成されたsceneStrategiesに順番に渡し、Sceneが返されるまでそれぞれに対してcalculateSceneを呼び出します。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 つの条件に基づいて有効になるリストと詳細レイアウトを作成する方法を示します。
- ウィンドウの幅が、2 つのペインをサポートするのに十分な幅(
WIDTH_DP_MEDIUM_LOWER_BOUND以上)である。 - バックスタックには、特定のメタデータを使用してリストと詳細レイアウトでの表示をサポートすることを宣言したエントリが含まれています。
次のスニペットは ListDetailScene.kt のソースコードで、ListDetailScene と ListDetailSceneStrategy の両方が含まれています。
// --- ListDetailScene --- /** * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. * */ data 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 アーティファクトは、リストと詳細の 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() }, 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") } } ) } } } }