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>>를 가져오는 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는 백 스택 키에서 파생된 현재NavEntrys목록을 구성된SceneStrategy's 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) ) }
예: 기본 목록-세부정보 레이아웃 (맞춤 장면 및 전략)
이 예에서는 다음 두 가지 조건에 따라 활성화되는 간단한 목록-세부정보 레이아웃을 만드는 방법을 보여줍니다.
- 창 너비가 두 개의 창을 지원할 만큼 충분히 넓습니다 (즉,
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. * */ 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.containsKey(DETAIL_KEY) } ?: return null val listEntry = entries.findLast { it.metadata.containsKey(LIST_KEY) } ?: 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 ) } companion object { internal const val LIST_KEY = "ListDetailScene-List" internal const val DETAIL_KEY = "ListDetailScene-Detail" /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = mapOf(LIST_KEY to true) /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = mapOf(DETAIL_KEY to true) } }
NavDisplay에서 이 ListDetailSceneStrategy를 사용하려면 목록 레이아웃으로 표시하려는 항목의 ListDetailScene.listPane() 메타데이터와 세부정보 레이아웃으로 표시하려는 항목의 ListDetailScene.detailPane()를 포함하도록 entryProvider 호출을 수정합니다. 그런 다음 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() }, sceneStrategy = 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) }
자체 목록 세부정보 장면을 만들고 싶지 않다면 다음 섹션에 표시된 대로 적절한 세부정보와 자리표시자 지원이 제공되는 Material 목록 세부정보 장면을 사용하면 됩니다.
Material 적응형 장면에서 목록-세부정보 콘텐츠 표시
목록-세부정보 사용 사례의 경우 androidx.compose.material3.adaptive:adaptive-navigation3 아티팩트는 목록-세부정보 Scene를 만드는 ListDetailSceneStrategy를 제공합니다. 이 Scene는 복잡한 다중 창 배치 (목록, 세부정보, 추가 창)를 자동으로 처리하고 창 크기와 기기 상태에 따라 조정합니다.
Material 목록-세부정보 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") } } ) } } } }