В 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). - В стеке возврата находятся записи, которые заявили о поддержке отображения в формате список-подробности с использованием определенных метаданных.
Ниже приведён исходный код файла 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) } }
Чтобы использовать ListDetailSceneStrategy в вашем NavDisplay , измените вызовы 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() }, 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 Design, которая включает в себя понятные детали и поддержку заполнителей, как показано в следующем разделе.
Отображение содержимого списка и сведений в адаптивной сцене Material Design
Для сценария использования списка и подробностей артефакт androidx.compose.material3.adaptive:adaptive-navigation3 предоставляет ListDetailSceneStrategy , который создает Scene для списка и подробностей. Эта Scene автоматически обрабатывает сложные многопанельные конфигурации (список, подробности и дополнительные панели) и адаптирует их в зависимости от размера окна и состояния устройства.
Для создания Scene типа "список-детали" в формате Material Design выполните следующие шаги:
- Добавьте зависимость : добавьте
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") } } ) } } } }