Как настроить доставку по запросу

Модули функций позволяют отделить определенные функции и ресурсы от основного модуля приложения и включить их в набор App Bundle. Благодаря Play Feature Delivery пользователи могут скачивать и устанавливать такие компоненты по запросу, уже после того как установили основной APK-файл приложения.

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

Помните, что этот тип модульности требует больше усилий и, возможно, рефакторинга существующего кода приложения. Тщательно продумайте, какие функции приложения лучше всего сделать доступными по запросу. Чтобы лучше понять, как оптимально использовать функции доставки по запросу, ознакомьтесь с рекомендациями по UX-дизайну.

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

На этой странице рассказывается, как добавить модуль функции в проект приложения и настроить его для доставки по запросу. Прежде чем начать, убедитесь, что вы используете Android Studio 3.5 или более поздней версии и плагин Android Gradle 3.5.0 или более поздней версии.

Как настроить новый модуль для доставки по запросу

Проще всего создать новый модуль функции с помощью Android Studio 3.5 или более поздней версии. Поскольку модули функций зависят от основного модуля приложения, их можно добавлять только в существующие проекты приложений.

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

  1. Если вы ещё этого не сделали, откройте проект приложения в IDE.
  2. В строке меню выберите File (Файл) > New (Создать) > New Module (Новый модуль).
  3. В диалоговом окне Create New Module (Создать модуль) выберите Dynamic Feature Module (Динамический модуль функции) и нажмите Next (Далее).
  4. В разделе Настройка нового модуля выполните следующие действия:
    1. В раскрывающемся меню выберите Базовый модуль приложения для проекта приложения.
    2. Укажите название модуля. IDE использует это название, чтобы идентифицировать модуль как подпроект Gradle в файле настроек Gradle. При сборке набора App Bundle Gradle использует последний элемент названия подпроекта, чтобы добавить атрибут <manifest split> в манифест модуля функции.
    3. Укажите название пакета модуля. По умолчанию Android Studio предлагает название пакета, которое объединяет название корневого пакета базового модуля и название модуля, указанное на предыдущем шаге.
    4. Выберите минимальный уровень API, который должен поддерживать модуль. Это значение должно совпадать со значением основного модуля.
  5. Нажмите Далее.
  6. В разделе Параметры скачивания модулей выполните следующие действия:

    1. Укажите название модуля (не более 50 символов). Платформа использует это название, чтобы идентифицировать модуль для пользователей, например когда запрашивает подтверждение на скачивание модуля. Поэтому в базовом модуле приложения должно быть название модуля в виде строкового ресурса, который можно перевести. При создании модуля в Android Studio среда разработки добавляет строковый ресурс в основной модуль и вставляет в манифест модуля функции следующую запись:

      <dist:module
          ...
          dist:title="@string/feature_title">
      </dist:module>
      
    2. В раскрывающемся меню Включение при установке выберите Не включать модуль при установке. Android Studio добавляет в манифест модуля следующий код:

      <dist:module ... >
        <dist:delivery>
            <dist:on-demand/>
        </dist:delivery>
      </dist:module>
      
    3. Установите флажок Объединение, если хотите, чтобы этот модуль был доступен на устройствах с Android 4.4 (уровень API 20) и более ранних версий и входил в состав нескольких APK. Это значит, что вы можете включить для этого модуля поведение по запросу и отключить объединение, чтобы он не устанавливался на устройства, которые не поддерживают скачивание и установку раздельных APK. Android Studio добавляет в манифест модуля следующий код:

      <dist:module ...>
          <dist:fusing dist:include="true | false" />
      </dist:module>
      
  7. Нажмите Готово.

После того как Android Studio создаст модуль, проверьте его содержимое на панели Project (Проект). Для этого в строке меню выберите View > Tool Windows > Project (Вид > Окна инструментов > Проект). Код, ресурсы и структура по умолчанию должны быть такими же, как и в стандартном модуле приложения.

Затем вам нужно будет реализовать функцию установки по запросу с помощью библиотеки Play Feature Delivery.

Как добавить библиотеку Play Feature Delivery в проект

Прежде всего добавьте в проект библиотеку Play Feature Delivery.

Запросить модуль по запросу

Когда приложению требуется модуль функции, оно может запросить его, пока находится в активном режиме, с помощью класса SplitInstallManager. При отправке запроса приложение должно указать название модуля, заданное элементом split в манифесте целевого модуля. Когда вы создаете модуль функции в Android Studio, система сборки использует указанное вами название модуля, чтобы добавить это свойство в манифест модуля во время компиляции. Подробнее о манифестах модулей функций…

Например, у вас есть приложение с модулем по запросу, который позволяет делать снимки с помощью камеры устройства и отправлять их в сообщениях. В манифесте этого модуля указано разрешение split="pictureMessages". В следующем примере используется SplitInstallManager для запроса модуля pictureMessages (вместе с дополнительным модулем для некоторых рекламных фильтров):

Kotlin

// Creates an instance of SplitInstallManager.
val splitInstallManager = SplitInstallManagerFactory.create(context)

// Creates a request to install a module.
val request =
    SplitInstallRequest
        .newBuilder()
        // You can download multiple on demand modules per
        // request by invoking the following method for each
        // module you want to install.
        .addModule("pictureMessages")
        .addModule("promotionalFilters")
        .build()

splitInstallManager
    // Submits the request to install the module through the
    // asynchronous startInstall() task. Your app needs to be
    // in the foreground to submit the request.
    .startInstall(request)
    // You should also be able to gracefully handle
    // request state changes and errors. To learn more, go to
    // the section about how to Monitor the request state.
    .addOnSuccessListener { sessionId -> ... }
    .addOnFailureListener { exception ->  ... }

Java

// Creates an instance of SplitInstallManager.
SplitInstallManager splitInstallManager =
    SplitInstallManagerFactory.create(context);

// Creates a request to install a module.
SplitInstallRequest request =
    SplitInstallRequest
        .newBuilder()
        // You can download multiple on demand modules per
        // request by invoking the following method for each
        // module you want to install.
        .addModule("pictureMessages")
        .addModule("promotionalFilters")
        .build();

splitInstallManager
    // Submits the request to install the module through the
    // asynchronous startInstall() task. Your app needs to be
    // in the foreground to submit the request.
    .startInstall(request)
    // You should also be able to gracefully handle
    // request state changes and errors. To learn more, go to
    // the section about how to Monitor the request state.
    .addOnSuccessListener(sessionId -> { ... })
    .addOnFailureListener(exception -> { ... });

Когда приложение запрашивает модуль по запросу, библиотека Play Feature Delivery использует стратегию "выстрелил и забыл". То есть он отправляет запрос на скачивание модуля на платформу, но не отслеживает, успешно ли выполнена установка. Чтобы продолжить путь пользователя после установки или корректно обработать ошибки, отслеживайте статус запроса.

Примечание. Вы можете запросить модуль функции, который уже установлен на устройстве. Если API обнаружит, что модуль уже установлен, запрос будет сразу считаться выполненным. Кроме того, после установки модуля Google Play автоматически обновляет его. Это значит, что при загрузке новой версии набора App Bundle платформа обновляет все установленные APK-файлы, относящиеся к вашему приложению. Подробнее о том, как управлять обновлениями приложений…

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

Как отложить установку модулей по запросу

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

Вы можете указать, что модуль нужно скачать позже, с помощью метода deferredInstall(), как показано ниже. Кроме того, в отличие от SplitInstallManager.startInstall(), для отложенной установки приложение не должно быть открыто на переднем плане.

Kotlin

// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(listOf("promotionalFilters"))

Java

// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(Arrays.asList("promotionalFilters"));

Запросы на отложенную установку выполняются по мере возможности, и вы не можете отслеживать их ход. Поэтому перед попыткой доступа к модулю, который вы указали для отложенной установки, необходимо проверить, установлен ли он. Если модуль нужен вам немедленно, запросите его с помощью SplitInstallManager.startInstall(), как показано в предыдущем разделе.

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

Чтобы обновлять индикатор выполнения, запускать намерение после установки или корректно обрабатывать ошибки запросов, необходимо отслеживать обновления статуса асинхронной задачи SplitInstallManager.startInstall(). Чтобы получать обновления по запросу на установку, зарегистрируйте прослушиватель и получите идентификатор сеанса для запроса, как показано ниже.

Kotlin

// Initializes a variable to later track the session ID for a given request.
var mySessionId = 0

// Creates a listener for request status updates.
val listener = SplitInstallStateUpdatedListener { state ->
    if (state.sessionId() == mySessionId) {
      // Read the status of the request to handle the state update.
    }
}

// Registers the listener.
splitInstallManager.registerListener(listener)

...

splitInstallManager
    .startInstall(request)
    // When the platform accepts your request to download
    // an on demand module, it binds it to the following session ID.
    // You use this ID to track further status updates for the request.
    .addOnSuccessListener { sessionId -> mySessionId = sessionId }
    // You should also add the following listener to handle any errors
    // processing the request.
    .addOnFailureListener { exception ->
        // Handle request errors.
    }

// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener)

Java

// Initializes a variable to later track the session ID for a given request.
int mySessionId = 0;

// Creates a listener for request status updates.
SplitInstallStateUpdatedListener listener = state -> {
    if (state.sessionId() == mySessionId) {
      // Read the status of the request to handle the state update.
    }
};

// Registers the listener.
splitInstallManager.registerListener(listener);

...

splitInstallManager
    .startInstall(request)
    // When the platform accepts your request to download
    // an on demand module, it binds it to the following session ID.
    // You use this ID to track further status updates for the request.
    .addOnSuccessListener(sessionId -> { mySessionId = sessionId; })
    // You should also add the following listener to handle any errors
    // processing the request.
    .addOnFailureListener(exception -> {
        // Handle request errors.
    });

// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener);

Как обрабатывать ошибки запросов

Помните, что установка модулей функций по запросу иногда может завершаться ошибкой, как и установка приложений. Сбой установки может быть вызван такими проблемами, как нехватка хранилища устройства, отсутствие подключения к сети или отсутствие входа в Google Play. Рекомендации о том, как сделать такие ситуации менее неприятными для пользователей, можно найти в руководстве по UX для доставки по запросу.

В коде необходимо обработать ошибки скачивания или установки модуля с помощью addOnFailureListener(), как показано ниже.

Kotlin

splitInstallManager
    .startInstall(request)
    .addOnFailureListener { exception ->
        when ((exception as SplitInstallException).errorCode) {
            SplitInstallErrorCode.NETWORK_ERROR -> {
                // Display a message that requests the user to establish a
                // network connection.
            }
            SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> checkForActiveDownloads()
            ...
        }
    }

fun checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
        .sessionStates
        .addOnCompleteListener { task ->
            if (task.isSuccessful) {
                // Check for active sessions.
                for (state in task.result) {
                    if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                        // Cancel the request, or request a deferred installation.
                    }
                }
            }
        }
}

Java

splitInstallManager
    .startInstall(request)
    .addOnFailureListener(exception -> {
        switch (((SplitInstallException) exception).getErrorCode()) {
            case SplitInstallErrorCode.NETWORK_ERROR:
                // Display a message that requests the user to establish a
                // network connection.
                break;
            case SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED:
                checkForActiveDownloads();
            ...
    });

void checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
        .getSessionStates()
        .addOnCompleteListener( task -> {
            if (task.isSuccessful()) {
                // Check for active sessions.
                for (SplitInstallSessionState state : task.getResult()) {
                    if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                        // Cancel the request, or request a deferred installation.
                    }
                }
            }
        });
}

В таблице ниже описаны состояния ошибки, которые может потребоваться обработать в приложении.

Код ошибки Описание Рекомендуемое действие
ACTIVE_SESSIONS_LIMIT_EXCEEDED Запрос отклонен, так как в настоящее время выполняется скачивание по крайней мере по одному существующему запросу. Проверьте, есть ли запросы, которые все ещё скачиваются, как показано в примере выше.
MODULE_UNAVAILABLE Google Play не может найти запрошенный модуль на основе текущей установленной версии приложения, устройства и аккаунта Google Play пользователя. Если у пользователя нет доступа к модулю, сообщите ему об этом.
INVALID_REQUEST Запрос получен Google Play, но он недействителен. Убедитесь, что в запросе указана полная и точная информация.
SESSION_NOT_FOUND Сеанс с указанным идентификатором не найден. Если вы пытаетесь отслеживать состояние запроса по идентификатору сеанса, убедитесь, что идентификатор указан правильно.
API_NOT_AVAILABLE Библиотека Play Feature Delivery не поддерживается на текущем устройстве. Это означает, что устройство не может скачивать и устанавливать функции по запросу. Для устройств с Android 4.4 (уровень API 20) или более ранней версией модули функций нужно включать во время установки, используя свойство манифеста dist:fusing. Подробнее о манифесте модуля функции…
NETWORK_ERROR Не удалось обработать запрос из-за ошибки сети. Предложите пользователю подключиться к сети или выбрать другую сеть.
ACCESS_DENIED Приложение не может зарегистрировать запрос из-за недостаточных разрешений. Обычно это происходит, когда приложение работает в фоновом режиме. Повторите запрос, когда приложение вернется в активный режим.
INCOMPATIBLE_WITH_EXISTING_SESSION Запрос содержит один или несколько модулей, которые уже были запрошены, но ещё не установлены. Создайте новый запрос, не включающий модули, которые уже были запрошены приложением, или дождитесь завершения установки всех запрошенных модулей, прежде чем повторить попытку.

Обратите внимание, что запрос уже установленного модуля не приводит к ошибке.

SERVICE_DIED Сервис, ответственный за обработку запроса, не работает. Повторите запрос.

Ваш SplitInstallStateUpdatedListener получает SplitInstallSessionState с этим кодом ошибки, статусом FAILED и идентификатором сеанса -1.

INSUFFICIENT_STORAGE На устройстве недостаточно свободного места для установки модуля функции. Сообщите пользователю, что у него недостаточно места для установки этой функции.
SPLITCOMPAT_VERIFICATION_ERROR, SPLITCOMPAT_EMULATION_ERROR, SPLITCOMPAT_COPY_ERROR Библиотеке SplitCompat не удалось загрузить модуль функции. Эти ошибки должны автоматически устраняться после следующего перезапуска приложения.
PLAY_STORE_NOT_FOUND Приложение Google Play не установлено на устройстве. Сообщите пользователю, что для скачивания этой функции требуется приложение Google Play.
APP_NOT_OWNED Приложение не было установлено через Google Play, и функцию нельзя скачать. Эта ошибка может возникнуть только при отложенных установках. Если вы хотите, чтобы пользователь приобрел приложение в Google Play, используйте метод startInstall(), который может получить необходимое подтверждение пользователя.
INTERNAL_ERROR В Google Play произошла внутренняя ошибка. Повторите запрос.

Если при скачивании модуля по запросу произошла ошибка, рекомендуем показать пользователю диалоговое окно с двумя вариантами: Повторить попытку (повторить запрос) и Отмена (отменить запрос). Для дополнительной поддержки вы также должны предоставить ссылку на Справку, которая направляет пользователей в Справочный центр Google Play.

Обработка обновлений статуса

После регистрации прослушивателя и записи идентификатора сеанса для запроса используйте StateUpdatedListener.onStateUpdate() для обработки изменений состояния, как показано ниже.

Kotlin

override fun onStateUpdate(state : SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.FAILED
        && state.errorCode() == SplitInstallErrorCode.SERVICE_DIED) {
       // Retry the request.
       return
    }
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.DOWNLOADING -> {
              val totalBytes = state.totalBytesToDownload()
              val progress = state.bytesDownloaded()
              // Update progress bar.
            }
            SplitInstallSessionStatus.INSTALLED -> {

              // After a module is installed, you can start accessing its content or
              // fire an intent to start an activity in the installed module.
              // For other use cases, see access code and resources from installed modules.

              // If the request is an on demand module for an Android Instant App
              // running on Android 8.0 (API level 26) or higher, you need to
              // update the app context using the SplitInstallHelper API.
            }
        }
    }
}

Java

@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.status() == SplitInstallSessionStatus.FAILED
        && state.errorCode() == SplitInstallErrorCode.SERVICE_DIES) {
       // Retry the request.
       return;
    }
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            case SplitInstallSessionStatus.DOWNLOADING:
              int totalBytes = state.totalBytesToDownload();
              int progress = state.bytesDownloaded();
              // Update progress bar.
              break;

            case SplitInstallSessionStatus.INSTALLED:

              // After a module is installed, you can start accessing its content or
              // fire an intent to start an activity in the installed module.
              // For other use cases, see access code and resources from installed modules.

              // If the request is an on demand module for an Android Instant App
              // running on Android 8.0 (API level 26) or higher, you need to
              // update the app context using the SplitInstallHelper API.
        }
    }
}

Возможные статусы запроса на установку описаны в таблице ниже.

Статус запроса Описание Рекомендуемое действие
В ОЖИДАНИИ Запрос принят, и скачивание скоро начнется. Инициализируйте компоненты интерфейса, например индикатор выполнения, чтобы пользователь мог отслеживать скачивание.
REQUIRES_USER_CONFIRMATION Для скачивания требуется подтверждение пользователя. Чаще всего это происходит, если приложение было установлено не через Google Play. Предложите пользователю подтвердить скачивание функции через Google Play. Подробнее о том, как получить подтверждение пользователя…
СКАЧИВАНИЕ Выполняется скачивание. Если вы используете индикатор выполнения скачивания, обновляйте интерфейс с помощью методов SplitInstallSessionState.bytesDownloaded() и SplitInstallSessionState.totalBytesToDownload() (см. пример кода над этой таблицей).
СКАЧАНО Модуль скачан, но установка ещё не началась. Чтобы получить доступ к скачанным модулям и избежать этого состояния, включите SplitCompat. Это необходимо для доступа к коду и ресурсам модуля функции.
УСТАНОВКА… На устройство устанавливается модуль. Обновите индикатор выполнения. Обычно это состояние длится недолго.
УСТАНОВЛЕНО Модуль установлен на устройстве. Получите доступ к коду и ресурсу в модуле, чтобы продолжить путь пользователя.

Если модуль предназначен для приложения Android с мгновенным запуском, работающего на Android 8.0 (API уровня 26) или более поздней версии, вам нужно использовать splitInstallHelper, чтобы обновить компоненты приложения с помощью нового модуля.

ОШИБКА Запрос не был выполнен до установки модуля на устройство. Предложите пользователю повторить запрос или отменить его.
ОТМЕНА Устройство отменяет запрос. Подробнее о том, как отменить запрос на установку…
ОТМЕНЕН Запрос отменен.

Получите подтверждение пользователя

В некоторых случаях Google Play может запрашивать подтверждение пользователя перед скачиванием. Например, если приложение установлено не из Google Play или вы пытаетесь скачать большой объем данных через мобильный интернет. В таких случаях запрос получает статус REQUIRES_USER_CONFIRMATION, и приложение должно получить подтверждение пользователя, прежде чем устройство сможет скачать и установить модули. Чтобы получить подтверждение, приложение должно показать пользователю следующее сообщение:

Kotlin

override fun onSessionStateUpdate(state: SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
        // Displays a confirmation for the user to confirm the request.
        splitInstallManager.startConfirmationDialogForResult(
          state,
          // an activity result launcher registered via registerForActivityResult
          activityResultLauncher)
    }
    ...
 }

Java

@Override void onSessionStateUpdate(SplitInstallSessionState state) {
    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
        // Displays a confirmation for the user to confirm the request.
        splitInstallManager.startConfirmationDialogForResult(
          state,
          // an activity result launcher registered via registerForActivityResult
          activityResultLauncher);
    }
    ...
 }

Вы можете зарегистрировать средство запуска результатов объекта activity, используя встроенный контракт ActivityResultContracts.StartIntentSenderForResult. Подробнее об API Activity Result…

Статус запроса обновляется в зависимости от ответа пользователя:

  • Если пользователь подтвердит действие, статус запроса изменится на PENDING и начнется скачивание.
  • Если пользователь отклонит запрос, его статус изменится на CANCELED.
  • Если пользователь не сделает выбор до закрытия диалогового окна, статус запроса останется REQUIRES_USER_CONFIRMATION. Приложение может снова предложить пользователю выполнить запрос.

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

Kotlin

registerForActivityResult(StartIntentSenderForResult()) { result: ActivityResult -> {
        // Handle the user's decision. For example, if the user selects "Cancel",
        // you may want to disable certain functionality that depends on the module.
    }
}

Java

registerForActivityResult(
    new ActivityResultContracts.StartIntentSenderForResult(),
    new ActivityResultCallback<ActivityResult>() {
        @Override
        public void onActivityResult(ActivityResult result) {
            // Handle the user's decision. For example, if the user selects "Cancel",
            // you may want to disable certain functionality that depends on the module.
        }
    });

Как отменить запрос на установку

Если приложению нужно отменить запрос до установки, оно может вызвать метод cancelInstall(), используя идентификатор сеанса запроса, как показано ниже.

Kotlin

splitInstallManager
    // Cancels the request for the given session ID.
    .cancelInstall(mySessionId)

Java

splitInstallManager
    // Cancels the request for the given session ID.
    .cancelInstall(mySessionId);

Доступ к модулям

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

Однако следует отметить, что в течение некоторого времени (в некоторых случаях дней) после загрузки модуля на платформе действуют следующие ограничения на доступ к содержимому модуля:

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

Включите SplitCompat

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

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

Объявите SplitCompatApplication в манифесте

Проще всего включить SplitCompat, объявив SplitCompatApplication как подкласс Application в манифесте приложения, как показано ниже.

<application
    ...
    android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
</application>

После установки приложения на устройство вы можете автоматически получать доступ к коду и ресурсам из скачанных модулей функций.

Вызов SplitCompat во время выполнения

Вы также можете включить SplitCompat в определенных действиях или сервисах во время выполнения. Включить SplitCompat таким образом необходимо, чтобы запускать действия, включенные в модули функций. Для этого переопределите attachBaseContext, как показано ниже.

Если у вас есть собственный класс Application, расширьте его с помощью SplitCompatApplication, чтобы включить SplitCompat для приложения, как показано ниже.

Kotlin

class MyApplication : SplitCompatApplication() {
    ...
}

Java

public class MyApplication extends SplitCompatApplication {
    ...
}

SplitCompatApplication просто переопределяет ContextWrapper.attachBaseContext(), чтобы включить SplitCompat.install(Context applicationContext). Если вы не хотите, чтобы класс Application расширял SplitCompatApplication, вы можете переопределить метод attachBaseContext() вручную, как показано ниже.

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this);
}

Если ваш модуль по запросу совместим как с приложениями с мгновенным запуском, так и с установленными приложениями, вы можете вызывать SplitCompat условно, как показано ниже.

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    if (!InstantApps.isInstantApp(this)) {
        SplitCompat.install(this)
    }
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    if (!InstantApps.isInstantApp(this)) {
        SplitCompat.install(this);
    }
}

Включите SplitCompat для действий модуля

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

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this);
}

Доступ к компонентам, определенным в модулях функций

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

После включения SplitCompat вы можете запускать действия, определенные в модулях функций, с помощью startActivity().

Kotlin

startActivity(Intent()
  .setClassName("com.package", "com.package.module.MyActivity")
  .setFlags(...))

Java

startActivity(new Intent()
  .setClassName("com.package", "com.package.module.MyActivity")
  .setFlags(...));

Первый параметр setClassName – это название пакета приложения, а второй – полное название класса действия.

Если у вас есть объект activity в модуле функции, который вы скачали по запросу, необходимо включить SplitCompat в этом объекте.

Запуск сервиса, определенного в модуле функции

После включения SplitCompat вы можете запускать сервисы, определенные в модулях функций, с помощью startService().

Kotlin

startService(Intent()
  .setClassName("com.package", "com.package.module.MyService")
  .setFlags(...))

Java

startService(new Intent()
  .setClassName("com.package", "com.package.module.MyService")
  .setFlags(...));

Экспорт компонента, определенного в модуле функции

Не включайте экспортированные компоненты Android в необязательные модули.

Система сборки объединяет записи манифеста для всех модулей в базовый модуль. Если бы дополнительный модуль содержал экспортированный компонент, он был бы доступен даже до установки модуля и мог бы вызвать сбой из-за отсутствия кода при вызове из другого приложения.

Для внутренних компонентов это не проблема, так как доступ к ним есть только у приложения, которое может проверить, установлен ли модуль, прежде чем обращаться к компоненту.

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

Доступ к коду и ресурсам из установленных модулей

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

Код доступа из другого модуля

Доступ к базовому коду из модуля

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

Как получить доступ к коду модуля из другого модуля

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

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

Чтобы упростить взаимодействие с объектом после создания экземпляра, рекомендуется определить интерфейс в базовом модуле и его реализацию в модуле функции. Пример:

Kotlin

// In the base module
interface MyInterface {
  fun hello(): String
}

// In the feature module
object MyInterfaceImpl : MyInterface {
  override fun hello() = "Hello"
}

// In the base module, where we want to access the feature module code
val stringFromModule = (Class.forName("com.package.module.MyInterfaceImpl")
    .kotlin.objectInstance as MyInterface).hello();

Java

// In the base module
public interface MyInterface {
  String hello();
}

// In the feature module
public class MyInterfaceImpl implements MyInterface {
  @Override
  public String hello() {
    return "Hello";
  }
}

// In the base module, where we want to access the feature module code
String stringFromModule =
   ((MyInterface) Class.forName("com.package.module.MyInterfaceImpl").getConstructor().newInstance()).hello();

Как получить доступ к ресурсам и объектам из другого модуля

После установки модуля вы можете получить доступ к ресурсам и объектам в нем стандартным способом, но с двумя оговорками:

  • Если вы обращаетесь к ресурсу из другого модуля, у этого модуля не будет доступа к идентификатору ресурса, но к ресурсу по-прежнему можно будет обратиться по названию. Обратите внимание, что пакет, который нужно использовать для ссылки на ресурс, – это пакет модуля, в котором определен ресурс.
  • Если вы хотите получить доступ к объектам или ресурсам, которые находятся в недавно установленном модуле, из другого установленного модуля приложения, используйте контекст приложения. Контекст компонента, который пытается получить доступ к ресурсам, ещё не будет обновлен. Вы также можете заново создать этот компонент (например, вызвав Activity.recreate()) или переустановить SplitCompat после установки модуля функции.

Как загрузить нативный код в приложение с помощью доставки по запросу

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

Загрузка нативного кода из необязательного модуля

После установки модуля рекомендуем загрузить его нативный код с помощью ReLinker. Для приложений с мгновенным запуском используйте этот метод.

Если вы используете System.loadLibrary() для загрузки нативного кода и ваша нативная библиотека зависит от другой библиотеки в модуле, сначала необходимо вручную загрузить эту другую библиотеку. Если вы используете ReLinker, эквивалентная операция – Relinker.recursively().loadLibrary().

Если вы используете dlopen() в нативном коде для загрузки библиотеки, определенной в необязательном модуле, относительные пути к библиотеке работать не будут. Лучшее решение – получить абсолютный путь к библиотеке из кода Java с помощью ClassLoader.findLibrary(), а затем использовать его в вызове dlopen(). Сделайте это до ввода нативного кода или используйте вызов JNI из нативного кода в Java.

Как получить доступ к установленным приложениям для Android с мгновенным запуском

После того как модуль приложения для Android с мгновенным запуском сообщит о статусе INSTALLED, вы сможете получить доступ к его коду и ресурсам, используя обновленный контекст приложения. Контекст, созданный приложением до установки модуля (например, уже сохраненный в переменной), не содержит контент нового модуля. Новый контекст, который можно получить, например, с помощью createPackageContext, не имеет такого ограничения.

Kotlin

// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                val newContext = context.createPackageContext(context.packageName, 0)
                // If you use AssetManager to access your app’s raw asset files, you’ll need
                // to generate a new AssetManager instance from the updated context.
                val am = newContext.assets
            }
        }
    }
}

Java

// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            ...
            case SplitInstallSessionStatus.INSTALLED:
                Context newContext = context.createPackageContext(context.getPackageName(), 0);
                // If you use AssetManager to access your app’s raw asset files, you’ll need
                // to generate a new AssetManager instance from the updated context.
                AssetManager am = newContext.getAssets();
        }
    }
}

Приложения для Android с мгновенным запуском на устройствах с Android 8.0 и более поздних версий

Если вы запрашиваете модуль по запросу для приложения Android с мгновенным запуском на устройстве с Android 8.0 (уровень API 26) или более поздней версии, то после того, как запрос на установку вернет статус INSTALLED, вам нужно обновить приложение с учетом контекста нового модуля, вызвав метод SplitInstallHelper.updateAppInfo(Context context). В противном случае приложение не будет знать о коде и ресурсах модуля. После обновления метаданных приложения загрузите содержимое модуля во время следующего события основного потока, вызвав новый Handler, как показано ниже:

Kotlin

override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                // You need to perform the following only for Android Instant Apps
                // running on Android 8.0 (API level 26) and higher.
                if (BuildCompat.isAtLeastO()) {
                    // Updates the app’s context with the code and resources of the
                    // installed module.
                    SplitInstallHelper.updateAppInfo(context)
                    Handler().post {
                        // Loads contents from the module using AssetManager
                        val am = context.assets
                        ...
                    }
                }
            }
        }
    }
}

Java

@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            ...
            case SplitInstallSessionStatus.INSTALLED:
            // You need to perform the following only for Android Instant Apps
            // running on Android 8.0 (API level 26) and higher.
            if (BuildCompat.isAtLeastO()) {
                // Updates the app’s context with the code and resources of the
                // installed module.
                SplitInstallHelper.updateAppInfo(context);
                new Handler().post(new Runnable() {
                    @Override public void run() {
                        // Loads contents from the module using AssetManager
                        AssetManager am = context.getAssets();
                        ...
                    }
                });
            }
        }
    }
}

Загрузка библиотек C/C++

Если вы хотите загрузить библиотеки C/C++ из модуля, который устройство уже скачало в приложении с мгновенным запуском, используйте SplitInstallHelper.loadLibrary(Context context, String libName), как показано ниже.

Kotlin

override fun onStateUpdate(state: SplitInstallSessionState) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.INSTALLED -> {
                // Updates the app’s context as soon as a module is installed.
                val newContext = context.createPackageContext(context.packageName, 0)
                // To load C/C++ libraries from an installed module, use the following API
                // instead of System.load().
                SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”)
                ...
            }
        }
    }
}

Java

public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            case SplitInstallSessionStatus.INSTALLED:
                // Updates the app’s context as soon as a module is installed.
                Context newContext = context.createPackageContext(context.getPackageName(), 0);
                // To load C/C++ libraries from an installed module, use the following API
                // instead of System.load().
                SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”);
                ...
        }
    }
}

Известные ограничения

  • Android WebView нельзя использовать в действии, которое обращается к ресурсам или объектам из дополнительного модуля. Это связано с несовместимостью WebView и SplitCompat на устройствах с Android API уровня 28 и ниже.
  • Вы не можете кешировать объекты ApplicationInfo Android, их содержимое или объекты, которые содержат их в вашем приложении. Вы всегда должны получать эти объекты по мере необходимости из контекста приложения. Кеширование таких объектов может привести к сбою приложения при установке модуля функции.

Управление установленными модулями

Чтобы узнать, какие модули функций установлены на устройстве, вызовите метод SplitInstallManager.getInstalledModules(). Он возвращает Set<String> с названиями установленных модулей, как показано ниже.

Kotlin

val installedModules: Set<String> = splitInstallManager.installedModules

Java

Set<String> installedModules = splitInstallManager.getInstalledModules();

Удаление модулей

Вы можете запросить удаление модулей с устройства, вызвав метод SplitInstallManager.deferredUninstall(List<String> moduleNames), как показано ниже.

Kotlin

// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(listOf("pictureMessages", "promotionalFilters"))

Java

// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(Arrays.asList("pictureMessages", "promotionalFilters"));

Модули удаляются не сразу. Это означает, что устройство удаляет их в фоновом режиме по мере необходимости, чтобы освободить место в хранилище. Чтобы убедиться, что модуль удален, вызовите SplitInstallManager.getInstalledModules() и проверьте результат, как описано в предыдущем разделе.

Скачать дополнительные языковые ресурсы

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

Если вы хотите, чтобы у приложения был доступ к дополнительным языковым ресурсам, например для реализации окна выбора языка в приложении, вы можете использовать библиотеку Play Feature Delivery, чтобы скачивать их по запросу. Процесс похож на скачивание модуля функции, как показано ниже.

Kotlin

// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply()
...

// Creates a request to download and install additional language resources.
val request = SplitInstallRequest.newBuilder()
        // Uses the addLanguage() method to include French language resources in the request.
        // Note that country codes are ignored. That is, if your app
        // includes resources for “fr-FR” and “fr-CA”, resources for both
        // country codes are downloaded when requesting resources for "fr".
        .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
        .build()

// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request)

Java

// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply();
...

// Creates a request to download and install additional language resources.
SplitInstallRequest request =
    SplitInstallRequest.newBuilder()
        // Uses the addLanguage() method to include French language resources in the request.
        // Note that country codes are ignored. That is, if your app
        // includes resources for “fr-FR” and “fr-CA”, resources for both
        // country codes are downloaded when requesting resources for "fr".
        .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
        .build();

// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request);

Запрос обрабатывается так же, как и запрос модуля функции. Вы можете отслеживать статус запроса как обычно.

Если дополнительные языковые ресурсы не требуются приложению немедленно, вы можете отложить установку до момента, когда приложение будет работать в фоновом режиме, как показано ниже.

Kotlin

splitInstallManager.deferredLanguageInstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))

Java

splitInstallManager.deferredLanguageInstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));

Как получить доступ к скачанным языковым ресурсам

Чтобы получить доступ к скачанным языковым ресурсам, ваше приложение должно запустить метод SplitCompat.installActivity() в методе attachBaseContext() каждого действия, которому требуется доступ к этим ресурсам, как показано ниже.

Kotlin

override fun attachBaseContext(base: Context) {
  super.attachBaseContext(base)
  SplitCompat.installActivity(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
  super.attachBaseContext(base);
  SplitCompat.installActivity(this);
}

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

Kotlin

override fun attachBaseContext(base: Context) {
  val configuration = Configuration()
  configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
  val context = base.createConfigurationContext(configuration)
  super.attachBaseContext(context)
  SplitCompat.install(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
  Configuration configuration = new Configuration();
  configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
  Context context = base.createConfigurationContext(configuration);
  super.attachBaseContext(context);
  SplitCompat.install(this);
}

Чтобы изменения вступили в силу, вам нужно заново создать действие после того, как новый язык будет установлен и готов к использованию. Для этого можно использовать метод Activity#recreate().

Kotlin

when (state.status()) {
  SplitInstallSessionStatus.INSTALLED -> {
      // Recreates the activity to load resources for the new language
      // preference.
      activity.recreate()
  }
  ...
}

Java

switch (state.status()) {
  case SplitInstallSessionStatus.INSTALLED:
      // Recreates the activity to load resources for the new language
      // preference.
      activity.recreate();
  ...
}

Удалите дополнительные языковые ресурсы

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

Kotlin

val installedLanguages: Set<String> = splitInstallManager.installedLanguages

Java

Set<String> installedLanguages = splitInstallManager.getInstalledLanguages();

Затем вы можете решить, какие языки удалить, используя метод deferredLanguageUninstall(), как показано ниже.

Kotlin

splitInstallManager.deferredLanguageUninstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))

Java

splitInstallManager.deferredLanguageUninstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));

Локальное тестирование установки модулей

Библиотека Play Feature Delivery позволяет локально тестировать следующие возможности приложения без подключения к Google Play:

На этой странице описано, как развернуть раздельные APK-файлы приложения на тестовом устройстве, чтобы Play Feature Delivery автоматически использовал их для симуляции запроса, скачивания и установки модулей из Google Play.

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

  • Скачайте и установите последнюю версию bundletool. Чтобы создать новый набор устанавливаемых APK из набора App Bundle, вам понадобится bundletool.

Создание набора APK-файлов

Если вы ещё не создали раздельные APK-файлы для своего приложения, выполните следующие действия:

  1. Создайте набор App Bundle для своего приложения одним из следующих способов:
  2. Используйте bundletool, чтобы сгенерировать набор APK-файлов для всех конфигураций устройств. Для этого выполните следующую команду:

    bundletool build-apks --local-testing
      --bundle my_app.aab
      --output my_app.apks
    

Флаг --local-testing добавляет в манифесты APK метаданные, которые сообщают библиотеке Play Feature Delivery, что для тестирования установки модулей функций нужно использовать локальные раздельные APK, не подключаясь к Google Play.

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

Создав набор APK-файлов с помощью флага --local-testing, используйте bundletool, чтобы установить базовую версию приложения и перенести дополнительные APK-файлы в локальное хранилище устройства. Оба действия можно выполнить с помощью следующей команды:

bundletool install-apks --apks my_app.apks

Теперь, когда вы запустите приложение и выполните путь пользователя, чтобы скачать и установить модуль функции, библиотека Play Feature Delivery будет использовать APK-файлы, которые bundletool перенес в локальное хранилище устройства.

Имитировать ошибку сети

Чтобы имитировать установку модулей из Google Play, библиотека Play Feature Delivery использует вместо SplitInstallManager альтернативный класс FakeSplitInstallManager. Когда вы используете bundletool с флагом --local-testing, чтобы создать набор APK-файлов и развернуть их на тестовом устройстве, в них включаются метаданные, которые указывают библиотеке Play Feature Delivery автоматически переключать вызовы API вашего приложения на FakeSplitInstallManager вместо SplitInstallManager.

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

Kotlin

// Creates an instance of FakeSplitInstallManager with the app's context.
val fakeSplitInstallManager = FakeSplitInstallManagerFactory.create(context)
// Tells Play Feature Delivery Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true)

Java

// Creates an instance of FakeSplitInstallManager with the app's context.
FakeSplitInstallManager fakeSplitInstallManager =
    FakeSplitInstallManagerFactory.create(context);
// Tells Play Feature Delivery Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true);