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

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

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

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

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

Эта страница поможет вам добавить функциональный модуль в проект приложения и настроить его для доставки по требованию. Прежде чем начать, убедитесь, что вы используете Android Studio 3.5 или более позднюю версию и подключаемый модуль Android Gradle 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-файлов. Это означает, что вы можете включить поведение по требованию для этого модуля и отключить объединение, чтобы исключить его из устройств, которые не поддерживают загрузку и установку разделенных APK. Android Studio вставляет в манифест модуля следующее, чтобы отразить ваш выбор:

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

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

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

Включите библиотеку доставки функций Play в свой проект.

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

Запросить модуль по требованию

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

Ява

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

Примечание. Можно запросить функциональный модуль, который уже установлен на устройстве. 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"))

Ява

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

Котлин

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

Ява

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

Ява

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 Запрос отклонен, поскольку в данный момент загружается хотя бы один существующий запрос. Проверьте, есть ли еще загружаемые запросы, как показано в примере выше.
МОДУЛЬ_UNAVAILABLE Google Play не может найти запрошенный модуль на основе текущей установленной версии приложения, устройства и учетной записи Google Play пользователя. Если у пользователя нет доступа к модулю, сообщите ему об этом.
INVALID_REQUEST Google Play получил запрос, но запрос недействителен. Убедитесь, что информация, включенная в запрос, является полной и точной.
SESSION_NOT_FOUND Сеанс для данного идентификатора сеанса не найден. Если вы пытаетесь отслеживать состояние запроса по его идентификатору сеанса, убедитесь, что идентификатор сеанса правильный.
API_NOT_AVAILABLE Библиотека доставки функций Play не поддерживается на текущем устройстве. То есть устройство не имеет возможности загружать и устанавливать функции по требованию. Для устройств под управлением Android 4.4 (уровень API 20) или ниже вам следует включать функциональные модули во время установки, используя свойство манифеста dist:fusing . Чтобы узнать больше, прочитайте о манифесте функционального модуля .
NETWORK_ERROR Запрос не выполнен из-за сетевой ошибки. Предложите пользователю либо установить сетевое соединение, либо перейти на другую сеть.
ДОСТУП ЗАПРЕЩЕН Приложение не может зарегистрировать запрос из-за недостаточности разрешений. Обычно это происходит, когда приложение находится в фоновом режиме. Попытайтесь выполнить запрос, когда приложение вернется на передний план.
НЕСОВМЕСТИМЫЙ_WITH_EXISTING_SESSION Запрос содержит один или несколько модулей, которые уже были запрошены, но еще не установлены. Либо создайте новый запрос, который не включает модули, которые ваше приложение уже запросило, либо подождите, пока все запрошенные в данный момент модули завершат установку, прежде чем повторять запрос.

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

SERVICE_DIED Служба, ответственная за обработку запроса, умерла. Повторите запрос.

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

НЕДОСТАТОЧНО_ХРАНИТЕЛЯ На устройстве недостаточно свободного места для установки функционального модуля. Сообщите пользователю, что у него недостаточно места для установки этой функции.
SPLITCOMPAT_VERIFICATION_ERROR, SPLITCOMPAT_EMULATION_ERROR, SPLITCOMPAT_COPY_ERROR SplitCompat не удалось загрузить функциональный модуль. Эти ошибки должны автоматически устраниться после следующего перезапуска приложения.
PLAY_STORE_NOT_FOUND Приложение Play Store не установлено на устройстве. Сообщите пользователю, что для загрузки этой функции требуется приложение Play Store.
ПРИЛОЖЕНИЕ_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.
            }
        }
    }
}

Ява

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

Ява

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

Ява

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)

Ява

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

Модули доступа

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

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

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

Включить СплитКомпат

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

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

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

Самый простой способ включить SplitCompat — объявить SplitCompatApplication как подкласс Application в манифесте вашего приложения, как показано ниже:

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

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

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

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

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

Котлин

class MyApplication : SplitCompatApplication() {
    ...
}

Ява

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

Ява

@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)
    }
}

Ява

@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)
}

Ява

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

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

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

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

Котлин

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

Ява

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

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

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

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

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

Котлин

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

Ява

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

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

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

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

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

Если вам нужен экспортированный компонент и вы хотите, чтобы его содержимое находилось в дополнительном модуле, рассмотрите возможность реализации шаблона прокси. Вы можете сделать это, добавив в базу компонент, экспортируемый через прокси; при доступе прокси-компонент может проверить наличие модуля, содержащего контент. Если модуль присутствует, прокси-компонент может запустить внутренний компонент из модуля через 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();

Ява

// 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 с мгновенным запуском

После того как модуль 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
            }
        }
    }
}

Ява

// 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 Instant App на Android 8.0 (уровень API 26) и более поздних версий после того, как запрос на установку сообщает об 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
                        ...
                    }
                }
            }
        }
    }
}

Ява

@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) , как показано ниже:

Котлин

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

Ява

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

Ява

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

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

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

Котлин

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

Ява

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

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

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

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

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

Котлин

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

Ява

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

Ява

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

Доступ к загруженным языковым ресурсам

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

Котлин

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

Ява

@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)
}

Ява

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

Ява

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

Ява

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

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

Котлин

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

Ява

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

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

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

На этой странице описано, как развернуть разделенные 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 использовать локальные разделенные APK-файлы для проверки установки функциональных модулей без подключения к Play Store.

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

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

bundletool install-apks --apks my_app.apks

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

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

Чтобы имитировать установку модулей из Play Store, библиотека доставки функций Play использует альтернативу SplitInstallManager , называемую FakeSplitInstallManager , для запроса модуля. Когда вы используете bundletool с флагом --local-testing для создания набора APK-файлов и развертывания их на тестовом устройстве, он включает метаданные, которые инструктируют библиотеку доставки функций Play автоматически переключать вызовы 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)

Ява

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