Navigation 3에서는 장면을 통해 앱의 UI 흐름을 관리하는 강력하고 유연한 시스템을 도입합니다. 장면을 사용하면 고도로 맞춤설정된 레이아웃을 만들고, 다양한 화면 크기에 맞게 조정하고, 복잡한 다중 창 환경을 원활하게 관리할 수 있습니다.
장면 이해하기
탐색 3에서 Scene
는 하나 이상의 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
가 올바른 이전 상태를 예상하고 전환할 수 있습니다. 이 상태는 다른 클래스 또는 키가 있는 장면일 수 있습니다.content: @Composable () -> Unit
:Scene
가entries
및 해당Scene
에만 해당하는 주변 UI 요소를 렌더링하는 방식을 정의하는 구성 가능한 함수입니다.
장면 전략 이해하기
SceneStrategy
는 백 스택의 지정된 NavEntry
목록을 정렬하고 Scene
로 전환하는 방법을 결정하는 메커니즘입니다. 기본적으로 SceneStrategy
는 현재 백 스택 항목이 표시되면 다음 두 가지 주요 질문을 스스로 던집니다.
- 이 항목으로
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개 창 레이아웃 (맞춤 장면 및 전략)
이 예에서는 두 가지 조건에 따라 활성화되는 간단한 두 창 레이아웃을 만드는 방법을 보여줍니다.
- 창 너비가 두 창을 지원하기에 충분히 넓습니다 (즉,
WIDTH_DP_MEDIUM_LOWER_BOUND
이상). - 백 스택의 상위 두 항목은 특정 메타데이터를 사용하여 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 } } }
NavDisplay
에서 이 TwoPaneSceneStrategy
를 사용하려면 entryProvider
호출을 수정하여 2개 창 레이아웃에 표시하려는 항목의 TwoPaneScene.twoPane()
메타데이터를 포함합니다. 그런 다음 TwoPaneSceneStrategy()
를 sceneStrategy
로 제공하고 단일 창 시나리오의 기본 대체를 사용합니다.
// 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() } } } ) }
Material 적응형 장면에서 목록 세부정보 콘텐츠 표시
목록 세부정보 사용 사례의 경우 androidx.compose.material3.adaptive:adaptive-navigation3
아티팩트는 목록 세부정보 Scene
를 만드는 ListDetailSceneStrategy
를 제공합니다. 이 Scene
는 복잡한 다중 창 배열 (목록, 세부정보, 추가 창)을 자동으로 처리하고 창 크기 및 기기 상태에 따라 조정합니다.
Material list-detail 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") } } ) } } } }