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
s, которые будут получены, если действие «назад» произойдет из текущей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>?
Этот метод берет текущий List<NavEntry<T>>
из стека возврата и обратный вызов onBack
. Он должен вернуть Scene<T>
, если он может успешно сформировать его из предоставленных записей, или null
если это невозможно.
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: 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) ) }
Пример: базовый двухпанельный макет (пользовательская сцена и стратегия)
В этом примере показано, как создать простой двухпанельный макет, который активируется на основе двух условий:
- Ширина окна достаточна для поддержки двух панелей (т. е. не менее
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.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 } } }
Чтобы использовать 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 = TwoPaneSceneStrategy<Any>(), onBack = { count -> repeat(count) { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } } ) }
Отображение содержимого списка-детали в адаптивной сцене
Для варианта использования list-detail артефакт androidx.compose.material3.adaptive:adaptive-navigation3
предоставляет ListDetailSceneStrategy
, который создает Scene
list-detail. Эта 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<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") } } ) } } } }