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– composable-функция, в которой вы определяете, какSceneотображает свойentriesи любые окружающие элементы пользовательского интерфейса, относящиеся к этомуScene.metadata: Map<String, Any>– предоставляет информацию о сцене другим компонентам библиотеки, напримерNavDisplay. По умолчанию возвращаетmetadataпоследнегоNavEntryвentries.
Реализуйте equals и hashCode в пользовательских сценах
Специальные реализации Scene должны правильно реализовывать equals и hashCode, чтобы поддерживать следующее:
- Переходы между состояниями сцены. Каждый раз, когда сцена рассчитывается по списку стратегий,
NavDisplayпроверяет, совпадает ли новая сцена с предыдущей. Если обнаруживается изменение, обновляетсяSeekableTransition, используемый для перехода между сценами, что, в свою очередь, влияет на состояние жизненного цикла сцен. - Управление оверлеями. Для
OverlayScene(например, диалоговых окон)NavDisplayиспользует сам объект сцены в качествеkey, чтобы отслеживать жизненные циклы оверлеев и анимацию выхода.
Убедитесь, что все свойства, определяющие контент сцены, такие как key, entries и previousEntries, включены в реализации equals и hashCode. Как правило, в реализации этих методов не следует использовать обратные вызовы (например, onBack), поскольку они могут изменять экземпляры, не меняя логическое состояние сцены.
Стратегии сцен
SceneStrategy – это механизм, который определяет, как заданный список NavEntry из обратного стека должен быть упорядочен и преобразован в Scene. По сути, когда SceneStrategy получает текущие записи обратного стека, он задает себе два ключевых вопроса:
- Can I create a
Scenefrom these entries? Если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.
Как сцены и стратегии сцен работают вместе
NavDisplay – это центральная composable-функция, которая отслеживает обратный стек и использует один или несколько объектов SceneStrategy, чтобы определить и отрисовать нужный объект Scene.
Параметр sceneStrategies в NavDisplay ожидает список экземпляров SceneStrategy, которые отвечают за вычисление Scene для показа. Если ни одна из стратегий не позволяет рассчитать Scene, NavDisplay автоматически использует SinglePaneSceneStrategy по умолчанию.
Вот как это работает:
- Когда вы добавляете или удаляете ключи из обратного стека (например, с помощью
backStack.add()илиbackStack.removeLastOrNull()),NavDisplayотслеживает эти изменения. NavDisplayпередает текущий списокNavEntry(полученный из ключей обратного стека) настроеннымsceneStrategiesпо порядку, вызываяcalculateSceneдля каждого, пока не будет возвращенScene.- Когда
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. * */ data 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) } } }
Чтобы использовать этот ListDetailSceneStrategy в NavDisplay, измените вызовы entryProvider, включив метаданные ListDetailScene.listPane() для записи, которую вы хотите показать в макете list, и ListDetailScene.detailPane() для записи, которую вы хотите показать в макете detail. Затем укажите 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 Design, которая поставляется с разумными деталями и поддержкой плейсхолдеров, как показано в следующем разделе.
Показ контента в виде списка и подробных сведений в адаптивной сцене Material
Для варианта использования со списком и подробными сведениями артефакт androidx.compose.material3.adaptive:adaptive-navigation3 предоставляет ListDetailSceneStrategy, который создает Scene со списком и подробными сведениями. Этот Scene автоматически обрабатывает сложные многопанельные макеты (список, детали и дополнительные панели) и адаптирует их в зависимости от размера окна и состояния устройства.
Чтобы создать список и подробные сведения Material Design Scene, выполните следующие действия:
- Добавьте зависимость. Включите
androidx.compose.material3.adaptive:adaptive-navigation3в файлbuild.gradle.ktsпроекта. - Определите записи с помощью метаданных
ListDetailSceneStrategy. ИспользуйтеlistPane(), detailPane()иextraPane(), чтобы отметитьNavEntrysдля правильного отображения на панели. ПомощникlistPane()также позволяет указатьdetailPlaceholder, когда ни один элемент не выбран. - Используйте
rememberListDetailSceneStrategy(). Эта composable-функция предоставляет предварительно настроенный объект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() }, 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") } } ) } } } }