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

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

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

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

Красным кругом выделена командная кнопка на виртуальной клавиатуре (значок галочки).
Рисунок 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.

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

Как создать функцию поставщика записей напрямую

Обычно функция 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") }
        }
    }
}

Как использовать DSL-модем entryProvider

Язык entryProvider позволяет упростить функцию lambda, поскольку вам не нужно проверять каждый тип ключа и создавать для него 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 на основе ограничений на уровне сцены и на уровне записи.

Подробнее о жизненных циклах в Compose можно узнать в статье Жизненный цикл в 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 и показывает контент.