Navigation 3 представляет мощную и гибкую систему управления пользовательским интерфейсом вашего приложения с помощью сцен . Сцены позволяют создавать настраиваемые макеты, адаптироваться к разным размерам экранов и легко управлять сложными многопанельными интерфейсами.
Понимать сцены
В Navigation 3 Scene — это фундаментальная единица, которая отображает один или несколько экземпляров NavEntry . Scene можно рассматривать как отдельное визуальное состояние или раздел пользовательского интерфейса, который может содержать и управлять отображением контента из стека элементов управления.
Каждый экземпляр Scene уникально идентифицируется своим key и классом самой Scene . Этот уникальный идентификатор имеет решающее значение, поскольку он управляет анимацией верхнего уровня при смене Scene .
Интерфейс Scene имеет следующие свойства:
-
key: Any: уникальный идентификатор для данного экземпляраScene. Этот ключ в сочетании с классомSceneобеспечивает уникальность, в первую очередь для целей анимации. -
entries: List<NavEntry<T>>: это список объектовNavEntry, за отображение которых отвечаетScene. Важно отметить, что если один и тот жеNavEntryотображается в несколькихScenesво время перехода (например, при переходе общего элемента), его содержимое будет отображаться только последней целевойScene, в которой он отображается. -
previousEntries: List<NavEntry<T>>: Это свойство определяетNavEntry, которые будут получены при выполнении действия «назад» из текущейScene. Это свойство необходимо для вычисления правильного предиктивного состояния возврата, позволяяNavDisplayпредвидеть и переходить к правильному предыдущему состоянию, которое может быть сценой с другим классом и/или ключом. -
content: @Composable () -> Unit: Это компонуемая функция, в которой вы определяете, какSceneотображает своиentriesи любые окружающие элементы пользовательского интерфейса, специфичные для этойScene.
Понимание стратегий сцены
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>?
Этот метод представляет собой функцию расширения SceneStrategyScope , которая извлекает текущий List<NavEntry<T>> из стека переходов назад. Он должен вернуть Scene<T> , если ему удалось сформировать его из предоставленных записей, или null , если это невозможно.
SceneStrategyScope отвечает за сохранение любых необязательных аргументов, которые могут понадобиться SceneStrategy , например, обратный вызов onBack .
SceneStrategy также предоставляет удобную функцию then infix, позволяющую объединять несколько стратегий в цепочку. Это создаёт гибкий конвейер принятия решений, где каждая стратегия может попытаться рассчитать Scene , а если не удаётся, делегирует задачу следующей в цепочке.
Как сцены и стратегии сцен работают вместе
NavDisplay — это центральный компонуемый элемент, который отслеживает ваш стек и использует SceneStrategy для определения и визуализации соответствующей Scene .
Параметр NavDisplay's sceneStrategy ожидает SceneStrategy , отвечающую за расчёт Scene для отображения. Если предоставленная стратегия (или цепочка стратегий) не позволяет рассчитать Scene , NavDisplay автоматически возвращается к использованию SinglePaneSceneStrategy по умолчанию.
Вот разбивка взаимодействия:
- Когда вы добавляете или удаляете ключи из своего стека переходов (например, с помощью
backStack.add()илиbackStack.removeLastOrNull()),NavDisplayотслеживает эти изменения. -
NavDisplayпередает текущий списокNavEntrys(полученный из ключей обратного стека) в настроенный методSceneStrategy's calculateScene. - Если
SceneStrategyуспешно возвращаетScene,NavDisplayотображаетcontentэтойScene.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). - Две верхние записи в стеке явно заявляют о своей поддержке отображения в двухпанельном макете с использованием определенных метаданных.
Следующий фрагмент представляет собой объединенный исходный код для 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() } Column(modifier = Modifier.weight(0.5f)) { secondEntry.Content() } } } 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) } } @Composable fun <T : Any> rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy<T> { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass return remember(windowSizeClass) { TwoPaneSceneStrategy(windowSizeClass) } } // --- 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>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { // 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.contentKey, secondEntry.contentKey) 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 , включив метаданные 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 = rememberTwoPaneSceneStrategy(), onBack = { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } ) }
Отображение содержимого списка-подробностей в адаптивной сцене
Для варианта использования «список-детализация» артефакт androidx.compose.material3.adaptive:adaptive-navigation3 предоставляет ListDetailSceneStrategy , который создаёт Scene «список-детализация». Эта Scene автоматически обрабатывает сложные многопанельные структуры (список, подробная информация и дополнительные панели) и адаптирует их в зависимости от размера окна и состояния устройства.
Чтобы создать Scene со списком материалов, выполните следующие действия:
- Добавьте зависимость : включите
androidx.compose.material3.adaptive:adaptive-navigation3в файлbuild.gradle.ktsвашего проекта. - Определите свои записи с помощью метаданных
ListDetailSceneStrategy: используйтеlistPane(), detailPane()иextraPane(), чтобы отметить элементыNavEntrysдля отображения на соответствующих панелях. Вспомогательный методlistPane()также позволяет указать плейсхолдерdetailPlaceholderесли ни один элемент не выбран. - Используйте
rememberListDetailSceneStrategy(): эта составная функция предоставляет предварительно настроеннуюListDetailSceneStrategy, которую может использоватьNavDisplay.
Следующий фрагмент представляет собой пример Activity демонстрирующий использование ListDetailSceneStrategy :
@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") } } ) } } } }