Настройка доставки по требованию

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

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

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

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

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

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

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

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

  1. Если вы еще этого не сделали, откройте свой проект приложения в IDE.
  2. Выберите в строке меню Файл > Создать > Новый модуль .
  3. В диалоговом окне «Создать новый модуль» выберите «Динамический модуль функций» и нажмите «Далее» .
  4. В разделе «Настройка нового модуля» выполните следующие действия:
    1. Выберите базовый модуль приложения для вашего проекта из выпадающего меню.
    2. Укажите имя модуля . IDE использует это имя для идентификации модуля как подпроекта Gradle в файле настроек Gradle . При сборке пакета приложения Gradle использует последний элемент имени подпроекта для внедрения атрибута <manifest split> в манифест модуля .
    3. Укажите имя пакета модуля. По умолчанию Android Studio предлагает имя пакета, которое объединяет корневое имя пакета базового модуля и имя модуля, указанное вами на предыдущем шаге.
    4. Выберите минимальный уровень API, который должен поддерживать модуль. Это значение должно совпадать со значением базового модуля.
  5. Нажмите «Далее» .
  6. В разделе «Параметры загрузки модуля» выполните следующие действия:

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

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

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

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

После того, как Android Studio завершит создание вашего модуля, самостоятельно проверьте его содержимое в панели «Проект» (выберите «Вид» > «Окна инструментов» > «Проект» в строке меню). Код, ресурсы и организация по умолчанию должны быть аналогичны таковым в стандартном модуле приложения.

Далее вам потребуется реализовать функцию установки по запросу, используя библиотеку Play Feature Delivery.

Включите библиотеку Play Feature Delivery в свой проект.

Прежде чем начать, необходимо добавить библиотеку Play Feature Delivery Library в свой проект.

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

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

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

Котлин

// 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 Library использует стратегию «запустил и забыл». То есть, она отправляет запрос на загрузку модуля на платформу, но не отслеживает, прошла ли установка успешно. Чтобы продвинуть пользовательский опыт после установки или корректно обрабатывать ошибки, убедитесь, что вы отслеживаете состояние запроса .

Примечание: Запрос на добавление модуля, уже установленного на устройстве, допустим. API мгновенно считает запрос выполненным, если обнаруживает, что модуль уже установлен. Кроме того, после установки модуля Google Play автоматически обновляет его. То есть, когда вы загружаете новую версию пакета вашего приложения, платформа обновляет все установленные APK-файлы, относящиеся к вашему приложению. Для получения дополнительной информации см. раздел «Управление обновлениями приложений» .

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

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

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

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

Котлин

// 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() для запроса его установки, как показано в предыдущем разделе.

Отслеживайте состояние запроса.

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

Котлин

// 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 Store. Рекомендации по корректной обработке подобных ситуаций с точки зрения пользователя можно найти в наших рекомендациях по UX-дизайну для доставки по запросу .

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

Котлин

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.
                    }
                }
            }
        });
}

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

Код ошибки Описание Предлагаемые действия
ПРЕВЫШЕН ЛИМИТ АКТИВНЫХ СЕССИЙ Запрос отклонен, поскольку в данный момент выполняется как минимум один запрос на загрузку. Проверьте, есть ли еще запросы, которые продолжают загрузку, как показано в приведенном выше примере.
MODUL_UNAVAILABLE Google Play не может найти запрошенный модуль, исходя из текущей установленной версии приложения, устройства и учетной записи пользователя в Google Play. Если у пользователя нет доступа к модулю, сообщите ему об этом.
INVALID_REQUEST Google Play получил запрос, но он недействителен. Убедитесь, что информация, содержащаяся в запросе, является полной и точной.
СЕССИЯ НЕ НАШЛА Сессия с заданным идентификатором сессии не найдена. Если вы пытаетесь отслеживать состояние запроса по его идентификатору сессии, убедитесь, что идентификатор сессии указан правильно.
API_НЕ_ДОСТУПЕН Библиотека функций Play не поддерживается на данном устройстве. Это означает, что устройство не может загружать и устанавливать функции по запросу. Для устройств под управлением Android 4.4 (уровень API 20) или ниже следует включать модули функций во время установки, используя свойство манифеста dist:fusing . Для получения дополнительной информации ознакомьтесь с манифестом модуля функций .
СЕТЬ_ОШИБКА Запрос не удался из-за сетевой ошибки. Предложите пользователю либо установить сетевое соединение, либо переключиться на другую сеть.
ДОСТУП ЗАПРЕЩЕН Приложение не может зарегистрировать запрос из-за недостаточных прав доступа. Это обычно происходит, когда приложение находится в фоновом режиме. Повторите запрос, когда приложение вернется на передний план.
НЕ СОВМЕСТИМО С СУЩЕСТВУЮЩЕЙ СЕССИЕЙ Запрос содержит один или несколько модулей, которые уже были запрошены, но еще не установлены. Либо создайте новый запрос, не включающий модули, которые ваше приложение уже запрашивало, либо дождитесь завершения установки всех запрошенных модулей, прежде чем повторять запрос.

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

СЕРВИС_УМЕР Служба, ответственная за обработку запроса, прекратила свою работу. Повторите запрос.

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

НЕДОСТАТОЧНОЕ_ПАМЯТЬ В устройстве недостаточно свободного места для установки дополнительного модуля. Сообщите пользователю, что у него недостаточно места для установки этой функции.
SPLITCOMPAT_VERIFICATION_ERROR, SPLITCOMPAT_EMULATION_ERROR, SPLITCOMPAT_COPY_ERROR SplitCompat не удалось загрузить модуль функций. Эти ошибки должны автоматически устраниться после следующего перезапуска приложения.
PLAY_STORE_NOT_FOUND Приложение Play Store не установлено на устройстве. Сообщите пользователю, что для загрузки этой функции требуется приложение Play Store.
APP_NOT_OWNED Приложение не было установлено из Google Play, и эту функцию невозможно загрузить. Эта ошибка может возникнуть только при отложенной установке. Если вы хотите, чтобы пользователь загрузил приложение из Google Play, используйте startInstall() , который позволит получить необходимое подтверждение от пользователя .
ВНУТРЕННЯЯ ОШИБКА В магазине Play Store произошла внутренняя ошибка. Повторите запрос.

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

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

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

Котлин

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.
        }
    }
}

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

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

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

НЕУСПЕШНЫЙ Запрос не был выполнен до того, как модуль был установлен на устройстве. Предложите пользователю либо повторить запрос, либо отменить его.
ОТМЕНА Устройство находится в процессе отмены запроса. Чтобы узнать больше, перейдите в раздел о том, как отменить запрос на установку .
ОТМЕНЕНО Запрос аннулирован.

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

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

Котлин

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);
    }
    ...
 }

Вы можете зарегистрировать средство запуска результатов действий, используя встроенный контракт ActivityResultContracts.StartIntentSenderForResult . См. API результатов действий .

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

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

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

Котлин

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() используя идентификатор сессии запроса, как показано ниже.

Котлин

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 для вашего приложения, как показано ниже:

Котлин

class MyApplication : SplitCompatApplication() {
    ...
}

Java

public class MyApplication extends SplitCompatApplication {
    ...
}

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

Котлин

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 условно следующим образом:

Котлин

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() следующим образом:

Котлин

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() .

Котлин

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

Java

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

Первым параметром функции setClassName является имя пакета приложения, а вторым — полное имя класса активности.

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

Запуск службы, определенной в функциональном модуле.

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

Котлин

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

Java

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

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

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

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

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

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

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

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

Доступ к коду из другого модуля

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

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

Доступ к коду модуля из другого модуля

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

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

Для упрощения взаимодействия с объектом после его создания рекомендуется определить интерфейс в базовом модуле, а его реализацию — в модуле функций. Например:

Котлин

// 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 можно узнать в разделе «Советы по Android JNI» .

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

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

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

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

Получите доступ к установленным приложениям Android Instant Apps.

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

Котлин

// 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 Instant Apps на Android 8.0 и выше

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

Котлин

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++ из модуля, который устройство уже загрузило в рамках Instant App, используйте SplitInstallHelper.loadLibrary(Context context, String libName) , как показано ниже:

Котлин

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 и ниже.
  • В приложении нельзя кэшировать объекты Android ApplicationInfo , их содержимое или объекты, содержащие эти объекты. Всегда следует получать эти объекты по мере необходимости из контекста приложения. Кэширование таких объектов может привести к сбою приложения при установке модуля функций.

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

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

Котлин

val installedModules: Set<String> = splitInstallManager.installedModules

Java

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

Удалите модули

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

Котлин

// 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() и проверив результат, как описано в предыдущем разделе.

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

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

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

Котлин

// 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);

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

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

Котлин

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

Java

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

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

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

Котлин

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 :

Котлин

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() .

Котлин

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();
  ...
}

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

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

Котлин

val installedLanguages: Set<String> = splitInstallManager.installedLanguages

Java

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

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

Котлин

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

Java

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

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

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

  • Запрашивать и контролировать установку модулей.
  • Обработка ошибок установки.
  • Используйте SplitCompat для доступа к модулям .

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

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

  • Загрузите и установите последнюю версию bundletool . bundletool необходим для создания нового набора устанавливаемых APK-файлов из пакета вашего приложения.

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

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

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

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

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

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

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

bundletool install-apks --apks my_app.apks

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

Имитация сетевой ошибки

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

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

Котлин

// 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);