Навигация с помощью модулей функций

Библиотека Dynamic Navigator расширяет функциональность компонента Jetpack Navigation, позволяя работать с целевыми страницами, определенными в модулях функций. Эта библиотека также обеспечивает беспроблемную установку модулей функций по запросу при переходе к этим пунктам назначения.

Настроить

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

Классный

dependencies {
    def nav_version = "2.10.2"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
    api "androidx.navigation:navigation-ui-ktx:$nav_version"
    api "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
}

Котлин

dependencies {
    val nav_version = "2.10.2"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
    api("androidx.navigation:navigation-ui-ktx:$nav_version")
    api("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")
}

Обратите внимание, что другие зависимости Navigation должны использовать конфигурации API, чтобы они были доступны для модулей функций.

Основные функции

Чтобы обеспечить поддержку модулей функций, сначала замените все экземпляры NavHostFragment в приложении на androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment:

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment"
    app:navGraph="@navigation/nav_graph"
    ... />

Затем добавьте атрибут app:moduleName к любым целевым объектам <activity>, <fragment> или <navigation> в графах навигации модуля com.android.dynamic-feature, которые связаны с DynamicNavHostFragment. Этот атрибут сообщает библиотеке Dynamic Navigator, что пункт назначения принадлежит модулю функции с указанным вами именем.

<fragment
    app:moduleName="myDynamicFeature"
    android:id="@+id/featureFragment"
    android:name="com.google.android.samples.feature.FeatureFragment"
    ... />

Когда вы переходите к одному из этих пунктов назначения, библиотека Dynamic Navigator сначала проверяет, установлен ли модуль функции. Если модуль функции уже есть, приложение переходит к месту назначения, как и ожидалось. Если модуль отсутствует, приложение показывает промежуточный фрагмент прогресса, пока модуль устанавливается. Реализация фрагмента по умолчанию показывает базовый интерфейс с индикатором выполнения и обрабатывает ошибки установки.

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

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

Целевые страницы, для которых не указан параметр app:moduleName, продолжают работать без изменений, как если бы в приложении использовался обычный параметр NavHostFragment.

Настройка фрагмента прогресса

Вы можете переопределить реализацию фрагмента прогресса для каждого графа навигации, установив для атрибута app:progressDestination идентификатор пункта назначения, который вы хотите использовать для обработки хода установки. Ваш собственный пункт назначения должен быть Fragment, производным от AbstractProgressFragment. Вам необходимо переопределить абстрактные методы для уведомлений о ходе установки, ошибках и других событиях. Затем вы можете показать ход установки в выбранном вами интерфейсе.

Класс DefaultProgressFragment в реализации по умолчанию использует этот API, чтобы показывать ход установки.

Отслеживание статуса запроса

Библиотека Dynamic Navigator позволяет реализовать UX-процесс, аналогичный описанному в разделе Рекомендации по UX для доставки по запросу, в котором пользователь остается в контексте предыдущего экрана, ожидая завершения установки. Это означает, что вам не нужно показывать промежуточный пользовательский интерфейс или фрагмент прогресса.

Экран с нижней панелью навигации и значком, который показывает, что модуль функции скачивается.
Рисунок 2. Экран, на котором показан ход скачивания с нижней панели навигации.

В этом случае вы несете ответственность за отслеживание и обработку всех состояний установки, изменений хода выполнения, ошибок и т. д.

Чтобы запустить этот неблокирующий процесс навигации, передайте объект DynamicExtras, содержащий DynamicInstallMonitor, в NavController.navigate(), как показано в следующем примере:

Kotlin

val navController = ...
val installMonitor = DynamicInstallMonitor()

navController.navigate(
    destinationId,
    null,
    null,
    DynamicExtras(installMonitor)
)

Java

NavController navController = ...
DynamicInstallMonitor installMonitor = new DynamicInstallMonitor();

navController.navigate(
    destinationId,
    null,
    null,
    new DynamicExtras(installMonitor);
)

Сразу после вызова navigate() проверьте значение installMonitor.isInstallRequired, чтобы узнать, привела ли попытка перехода к установке модуля функции.

  • Если значение равно false, вы переходите к обычному пункту назначения и вам больше ничего не нужно делать.
  • Если значение равно true, начните наблюдать за объектом LiveData, который теперь находится в installMonitor.status. Этот объект LiveData передает обновления SplitInstallSessionState из библиотеки Play Core. Эти обновления содержат события, связанные с ходом установки, которые можно использовать для обновления пользовательского интерфейса. Не забудьте обработать все статусы, как описано в руководстве по Play Core, в том числе запрос подтверждения от пользователя, если это необходимо.

    Kotlin

    val navController = ...
    val installMonitor = DynamicInstallMonitor()
    
    navController.navigate(
      destinationId,
      null,
      null,
      DynamicExtras(installMonitor)
    )
    
    if (installMonitor.isInstallRequired) {
      installMonitor.status.observe(this, object : Observer<SplitInstallSessionState> {
          override fun onChanged(sessionState: SplitInstallSessionState) {
              when (sessionState.status()) {
                  SplitInstallSessionStatus.INSTALLED -> {
                      // Call navigate again here or after user taps again in the UI:
                      // navController.navigate(destinationId, destinationArgs, null, null)
                  }
                  SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> {
                      SplitInstallManager.startConfirmationDialogForResult(...)
                  }
    
                  // Handle all remaining states:
                  SplitInstallSessionStatus.FAILED -> {}
                  SplitInstallSessionStatus.CANCELED -> {}
              }
    
              if (sessionState.hasTerminalStatus()) {
                  installMonitor.status.removeObserver(this);
              }
          }
      });
    }

    Java

    NavController navController = ...
    DynamicInstallMonitor installMonitor = new DynamicInstallMonitor();
    
    navController.navigate(
      destinationId,
      null,
      null,
      new DynamicExtras(installMonitor);
    )
    
    if (installMonitor.isInstallRequired()) {
      installMonitor.getStatus().observe(this, new Observer<SplitInstallSessionState>() {
          @Override
          public void onChanged(SplitInstallSessionState sessionState) {
              switch (sessionState.status()) {
                  case SplitInstallSessionStatus.INSTALLED:
                      // Call navigate again here or after user taps again in the UI:
                      // navController.navigate(mDestinationId, mDestinationArgs, null, null);
                      break;
                  case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION:
                      SplitInstallManager.startConfirmationDialogForResult(...)
                      break;
    
                  // Handle all remaining states:
                  case SplitInstallSessionStatus.FAILED:
                      break;
                  case SplitInstallSessionStatus.CANCELED:
                      break;
              }
    
              if (sessionState.hasTerminalStatus()) {
                  installMonitor.getStatus().removeObserver(this);
              }
          }
      });
    }

После завершения установки объект LiveData возвращает статус SplitInstallSessionStatus.INSTALLED. Затем снова позвоните по номеру NavController.navigate(). Поскольку модуль теперь установлен, вызов выполняется успешно и приложение переходит к месту назначения, как и ожидалось.

После достижения конечного состояния, например когда установка завершена или произошел сбой, удалите наблюдатель LiveData, чтобы избежать утечки памяти. Проверить, является ли статус конечным, можно с помощью метода SplitInstallSessionStatus.hasTerminalStatus().

Пример реализации этого наблюдателя можно найти в AbstractProgressFragment.

Включенные графики

Библиотека Dynamic Navigator поддерживает включение графов, определенных в модулях функций. Чтобы включить граф, определенный в модуле функции, выполните следующие действия:

  1. Используйте <include-dynamic/> вместо <include/>, как показано в следующем примере:

    <include-dynamic
        android:id="@+id/includedGraph"
        app:moduleName="includedgraphfeature"
        app:graphResName="included_feature_nav"
        app:graphPackage="com.google.android.samples.dynamic_navigator.included_graph_feature" />
    
  2. Внутри <include-dynamic ... /> необходимо указать следующие атрибуты:

    • app:graphResName – название файла ресурса графа навигации. Название берется из имени файла графика. Например, если граф находится в res/navigation/nav_graph.xml, название ресурса – nav_graph.
    • android:id – идентификатор целевой страницы графа. Библиотека Dynamic Navigator игнорирует любые значения android:id, найденные в корневом элементе включенного графа.
    • app:moduleName – название пакета модуля.

Используйте правильный graphPackage

Важно правильно указать app:graphPackage, иначе компонент Navigation не сможет включить указанный navGraph из модуля функции.

Название пакета динамического модуля функции состоит из названия модуля и applicationId основного модуля приложения. Например, если у базового модуля приложения applicationId – com.example.dynamicfeatureapp, а модуль динамической функции называется DynamicFeatureModule, то название пакета динамического модуля будет com.example.dynamicfeatureapp.DynamicFeatureModule. Название пакета указывается с учетом регистра.

Если вы сомневаетесь, то можете проверить название пакета модуля функции в созданном файле AndroidManifest.xml. После сборки проекта перейдите по адресу <DynamicFeatureModule>/build/intermediates/merged_manifest/debug/AndroidManifest.xml. Вы увидите примерно следующее:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    featureSplit="DynamicFeatureModule"
    package="com.example.dynamicfeatureapp"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="30" />

    <dist:module
        dist:instant="false"
        dist:title="@string/title_dynamicfeaturemodule" >
        <dist:delivery>
            <dist:install-time />
        </dist:delivery>

        <dist:fusing dist:include="true" />
    </dist:module>

    <application />

</manifest>

Значение featureSplit должно совпадать с названием модуля функции, а пакет – с applicationId основного модуля приложения. app:graphPackage – это сочетание следующих элементов: com.example.dynamicfeatureapp.DynamicFeatureModule.

Перейти можно только к startDestination графа навигации include-dynamic. Динамический модуль отвечает за собственный граф навигации, а основное приложение не имеет к нему доступа.

Механизм include-dynamic позволяет основному модулю приложения включать вложенный граф навигации, определенный в динамическом модуле. Этот вложенный граф навигации ведет себя как любой вложенный граф навигации. Корневой граф навигации (то есть родительский элемент вложенного графа) может определять только сам вложенный граф навигации в качестве пункта назначения, а не его дочерние элементы. Таким образом, startDestination используется, когда целевым является граф include-dynamicnavigation.

Ограничения

  • Динамически включаемые графы в настоящее время не поддерживают ссылки на контент.
  • Динамически загружаемые вложенные графы (то есть элемент <navigation> с атрибутом app:moduleName) в настоящее время не поддерживают ссылки на контент.