В 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 – это механизм, который определяет, как должен быть организован и передан в Scene список объектов NavEntry из стека возврата. По сути, когда 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>> из стека возврата. Если из предоставленных записей можно сформировать URL, функция должна вернуть значение 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() для записи, которую вы хотите показать в макете списка, и 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() }, 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 list-detail, в которой есть подходящие детали и поддержка плейсхолдеров, как показано в следующем разделе.
Как показывать контент в формате "список и подробные сведения" в адаптивной сцене 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") } } ) } } } }