Navigation 3은 장면 을 통해 앱의 UI 흐름을 관리하는 강력하고 유연한 시스템을 도입합니다. 장면을 사용하면 고도로 맞춤설정된 레이아웃을 만들고, 다양한 화면 크기에 맞게 조정하고, 복잡한 다중 창 환경을 원활하게 관리할 수 있습니다.
장면 이해
Navigation 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 요소를 렌더링하는 방법을 정의하는 컴포저블 함수입니다.metadata: Map<String, Any>:NavDisplay와 같은 다른 라이브러리 구성요소에 장면별 정보를 제공합니다. 기본적으로entries의 마지막NavEntry의metadata를 반환합니다.
장면 전략 이해
SceneStrategy는 백 스택의 지정된
NavEntry 목록을
Scene으로 정렬하고 전환하는 방법을 결정하는 메커니즘입니다. 기본적으로 현재 백 스택 항목이 표시되면 SceneStrategy는 다음과 같은 두 가지 주요 질문을 합니다.
- 이 항목에서
Scene을 만들 수 있나요?SceneStrategy가 지정된NavEntry를 처리하고 의미 있는Scene(예: 대화상자 또는 다중 창 레이아웃)을 형성할 수 있다고 판단하면 진행합니다. 그렇지 않으면null을 반환하여 다른 전략이Scene을 만들 수 있도록 합니다. - 그렇다면 이러한 항목을
Scene?으로 어떻게 정렬해야 할까요?SceneStrategy가 항목 처리를 커밋하면 을 구성하고 지정된NavEntry가 해당Scene내에 표시되는 방식을 정의할 책임이 있습니다.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 if it
반환해야 합니다.
SceneStrategyScope는 SceneStrategy에 필요할 수 있는 선택적 인수
(예: onBack 콜백)를 유지관리합니다.
장면과 장면 전략이 함께 작동하는 방식
NavDisplay는 백 스택을 관찰하고
하나 이상의 SceneStrategy를 사용하여 적절한
Scene를 결정하고 렌더링하는 중앙 컴포저블입니다.
NavDisplay의 sceneStrategies 매개변수는 표시할 Scene을 계산하는 SceneStrategy
인스턴스 목록을 예상합니다. 제공된 전략으로
Scene이 계산되지 않으면 NavDisplay는 기본적으로
을 사용하도록 자동으로SinglePaneSceneStrategy 대체됩니다.
상호작용에 대한 분석은 다음과 같습니다.
- 백 스택에서 키를 추가하거나 삭제하면 (
backStack.add()또는backStack.removeLastOrNull()사용)NavDisplay가 이러한 변경사항을 관찰합니다. NavDisplay는Scene이 반환될 때까지 각 항목에서calculateScene을 호출하여 백 스택 키에서 파생된 현재NavEntry목록을 구성된sceneStrategies에 순서대로 전달합니다.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.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를 사용하려면 목록 레이아웃으로 표시하려는 항목의 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() }, 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) }
자체 목록-세부정보 장면을 만들고 싶지 않다면 다음 섹션에서 보여주는 것처럼 적절한 세부정보와 자리표시자 지원이 제공되는 Material 목록-세부정보 장면을 사용할 수 있습니다.
Material 적응형 장면에서 목록-세부정보 콘텐츠 표시
목록-세부정보 사용 사례의 경우
androidx.compose.material3.adaptive:adaptive-navigation3 아티팩트는
ListDetailSceneStrategy 목록-세부정보 Scene을(를) 만드는 기능을 제공합니다. 이 Scene은 복잡한 다중 창 정렬 (목록, 세부정보, 추가 창)을 자동으로 처리하고 창 크기와 기기 상태에 따라 조정합니다.
Material 목록-세부정보 Scene을 만들려면 다음 단계를 따르세요.
- 종속 항목 추가: 프로젝트의
build.gradle.kts파일에androidx.compose.material3.adaptive:adaptive-navigation3을 포함합니다. ListDetailSceneStrategy메타데이터로 항목 정의:listPane(), detailPane(),extraPane()을 사용하여 적절한 창 표시를 위해NavEntrys을 표시합니다.listPane()도우미를 사용하면 항목이 선택되지 않은 경우detailPlaceholder를 지정할 수도 있습니다.rememberListDetailSceneStrategy() 사용 : 이 컴포저블 함수는 사전 구성된ListDetailSceneStrategy를 제공하며NavDisplay에서 사용할 수 있습니다.
다음 스니펫은 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") } } ) } } } }