Освойте основы

Навигация – это способ перемещения пользователей по приложению. Пользователи взаимодействуют с элементами интерфейса, обычно нажимая на них, а приложение отвечает, отображая новый контент. Если пользователь хочет вернуться к предыдущему контенту, он использует жест назад или нажимает кнопку назад.

Моделирование состояния навигации

Удобный способ смоделировать это поведение – использовать стек контента. Когда пользователь переходит вперед к новому контенту, он помещается в верхнюю часть стека. Когда пользователь нажимает кнопку Назад, контент удаляется из стека и показывается предыдущий. В терминах навигации этот стек обычно называют обратным стеком, поскольку он представляет контент, к которому пользователь может вернуться.

Командная кнопка виртуальной клавиатуры (значок галочки), обведенная красным кругом.
Рисунок 1. Диаграмма, показывающая, как меняется обратный стек при навигации пользователя.

Как создать обратный стек

В Navigation 3 обратный стек не содержит контента. Вместо этого он содержит ссылки на контент, которые называются ключами. Ключи могут быть любого типа, но обычно это простые сериализуемые классы данных. Использование цифровых отпечатков вместо контента дает следующие преимущества:

  • Навигация осуществляется просто – путем добавления ключей в обратный стек.
  • Если ключи сериализуемы, обратный стек можно сохранить в постоянном хранилище, чтобы он не терялся при изменении конфигурации и завершении процесса. Это важно, потому что пользователи ожидают, что, когда они вернутся в приложение, оно будет в том же состоянии, в котором они его оставили. Подробнее о том, как сохранить обратный стек…

Ключевая концепция Navigation 3 API заключается в том, что вы владеете обратным стеком. Библиотека

  • Ожидается, что обратный стек будет иметь тип List<T>, где T – тип keys обратного стека. Вы можете использовать Any или предоставить собственные ключи со строгой типизацией. Когда вы видите термины "push" или "pop", базовая реализация заключается в добавлении или удалении элементов из конца списка.
  • Отслеживает обратный стек и отражает его состояние в интерфейсе с помощью NavDisplay.

В следующем примере показано, как создавать ключи и обратный стек, а также изменять обратный стек в ответ на события навигации пользователя:

// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)

@Composable
fun MyApp() {

    // Create a back stack, specifying the key the app should start with
    val backStack = remember { mutableStateListOf<Any>(ProductList) }

    // Supply your back stack to a NavDisplay so it can reflect changes in the UI
    // ...more on this below...

    // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
    backStack.add(ProductDetail(id = "ABC"))

    // Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
    backStack.removeLastOrNull()
}

Сопоставление ключей с контентом

Контент в Navigation 3 моделируется с помощью NavEntry – класса, содержащего composable-функцию. Он представляет собой целевой объект – отдельный фрагмент контента, к которому пользователь может перейти и от которого может вернуться.

NavEntry также может содержать метаданные – информацию о контенте. Эти метаданные могут считываться объектами-контейнерами, такими как NavDisplay, чтобы определить, как показывать контент NavEntry. Например, с помощью метаданных можно переопределить анимацию по умолчанию для определенного элемента NavEntry. NavEntry metadata – это карта ключей String и значений Any, которая обеспечивает универсальное хранение данных.

Чтобы преобразовать key в NavEntry, создайте поставщика записей. Это функция, которая принимает key и возвращает NavEntry для этого key. Обычно он определяется как лямбда-параметр при создании NavDisplay.

Создать Entry Provider можно двумя способами: напрямую с помощью лямбда-функции или с помощью DSL entryProvider.

Создайте функцию Entry Provider напрямую

Обычно функция Entry Provider создается с помощью оператора when, в котором для каждого ключа есть своя ветвь.

entryProvider = { key ->
    when (key) {
        is ProductList -> NavEntry(key) { Text("Product List") }
        is ProductDetail -> NavEntry(
            key,
            metadata = mapOf("extraDataKey" to "extraDataValue")
        ) { Text("Product ${key.id} ") }

        else -> {
            NavEntry(Unit) { Text(text = "Invalid Key: $it") }
        }
    }
}

Используйте entryProvider DSL

DSL entryProvider позволяет упростить лямбда-функцию, поскольку вам не нужно тестировать каждый тип ключа и создавать для него NavEntry. Для этого используйте функцию entryProvider. Также в нем есть резервное поведение по умолчанию (выдача ошибки), если ключ не найден.

entryProvider = entryProvider {
    entry<ProductList> { Text("Product List") }
    entry<ProductDetail>(
        metadata = mapOf("extraDataKey" to "extraDataValue")
    ) { key -> Text("Product ${key.id} ") }
}

Обратите внимание на следующие элементы фрагмента:

  • entry используется для определения NavEntry с заданным типом и составным контентом.
  • entry принимает параметр metadata, чтобы задать NavEntry.metadata

Показать обратный стек

Обратный стек представляет состояние навигации в приложении. При каждом изменении обратного стека интерфейс приложения должен отражать его новое состояние. В Navigation 3 NavDisplay отслеживает обратный стек и обновляет интерфейс. Используйте следующие параметры:

  • Обратный стек. Он должен иметь тип SnapshotStateList<T>, где T – тип ключей обратного стека. Это наблюдаемый объект List, поэтому при его изменении запускается рекомпозиция NavDisplay.
  • entryProvider для преобразования ключей в обратном стеке в объекты NavEntry.
  • При необходимости укажите лямбда-выражение для параметра onBack. Этот метод вызывается, когда пользователь запускает событие возврата.

В примере ниже показано, как создать объект NavDisplay.

data object Home
data class Product(val id: String)

@Composable
fun NavExample() {

    val backStack = remember { mutableStateListOf<Any>(Home) }

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = { key ->
            when (key) {
                is Home -> NavEntry(key) {
                    ContentGreen("Welcome to Nav3") {
                        Button(onClick = {
                            backStack.add(Product("123"))
                        }) {
                            Text("Click to navigate")
                        }
                    }
                }

                is Product -> NavEntry(key) {
                    ContentBlue("Product ${key.id} ")
                }

                else -> NavEntry(Unit) { Text("Unknown route") }
            }
        }
    )
}

По умолчанию NavDisplay показывает самый верхний NavEntry в обратном стеке в макете с одной панелью. На видео ниже показано, как работает это приложение:

Поведение `NavDisplay` по умолчанию с двумя пунктами назначения.
Рисунок 2. NavDisplay режим работы по умолчанию с двумя целевыми сервисами.

Жизненный цикл целевой страницы

NavDisplay использует пользовательские LifecycleOwner, чтобы ограничить состояние жизненного цикла NavEntry на основе ограничений на уровне сцены и записи.

Подробнее о жизненном цикле в Jetpack Compose…

Ограничения жизненного цикла на уровне сцены

NavDisplay управляет жизненным циклом активных объектов Scene. Ограничения на уровне сцены определяются следующим образом:

Для сцен без оверлеев:

  • RESUMED. Разрешено только после завершения перехода между сценами и при отсутствии активных оверлеев.
  • STARTED: ограничено до STARTED во время перехода между сценами, например при навигации вперед или назад, или когда оно закрыто оверлеем.

Для накладывающихся сцен, таких как диалоговые окна или нижние экраны:

  • RESUMED. Разрешено только для самой верхней, активной в данный момент сцены наложения.
  • STARTED: ограничено значением STARTED для всех базовых сцен оверлея, которые перекрываются более новым оверлеем.

Статус жизненного цикла начального уровня

Библиотека управляет максимальным состоянием жизненного цикла каждого отдельного элемента NavEntry в зависимости от его наличия в обратном стеке:

  • RESUMED. Если запись присутствует в текущем обратном стеке, ее жизненный цикл может доходить до RESUMED (с учетом ограничения на уровне сцены).
  • CREATED. Если запись больше не находится в обратном стеке, например когда она была извлечена, но все ещё отображается на экране во время анимации, библиотека строго ограничивает ее жизненный цикл на уровне CREATED. Это ограничение гарантирует, что фоновые или завершающие работу записи прекратят выполнение активных задач, таких как сбор потоков или запуск сопрограмм, связанных с состояниями RESUMED или STARTED, пока они завершают свои переходы выхода.

Как они сочетаются

Например, окончательное состояние жизненного цикла NavEntry определяется следующим образом:

Сценарий Ограничение на уровне сцены Ограничение для начального уровня Эффективное ограничение
Активная запись, экран в покое (без переходов и наложений) RESUMED RESUMED RESUMED
Активная запись во время перехода (переход к записи или от нее) STARTED RESUMED STARTED
Активная запись, закрытая оверлеем (например, открыто диалоговое окно). STARTED RESUMED STARTED
Всплывающее появление, анимированное исчезновение STARTED или RESUMED CREATED CREATED

Подведем итоги

На следующей диаграмме показано, как данные передаются между различными объектами в Navigation 3:

Визуализация потока данных между различными объектами в Navigation 3.
Рисунок 3. Диаграмма, показывающая, как данные передаются между различными объектами в Navigation 3.
  1. События навигации инициируют изменения. Ключи добавляются в обратный стек или удаляются из него в зависимости от действий пользователя.

  2. Изменение состояния обратного стека запускает получение контента. NavDisplay (composable-функция, которая отрисовывает обратный стек) наблюдает за обратным стеком. В конфигурации по умолчанию он отображает самую верхнюю запись обратного стека в макете с одной панелью. Когда верхний ключ в обратном стеке меняется, NavDisplay использует его, чтобы запросить соответствующий контент у поставщика записей.

  3. Поставщик контента предоставляет контент. Поставщик записей – это функция, которая сопоставляет ключ с NavEntry. Получив ключ от NavDisplay, поставщик записи предоставляет связанный с ним объект NavEntry, который содержит и ключ, и контент.

  4. Контент показывается. NavDisplay получает NavEntry и показывает контент.