Модули функций позволяют отделить определённые функции и ресурсы от базового модуля вашего приложения и включить их в комплект приложения. Например, благодаря Play Feature Delivery пользователи могут загружать и устанавливать эти компоненты по запросу после установки базового APK-файла вашего приложения.
Например, представьте себе приложение для обмена текстовыми сообщениями, которое позволяет делать снимки и отправлять их, но лишь небольшой процент пользователей отправляет их. Возможно, имеет смысл включить функцию обмена изображениями в качестве загружаемого модуля. Таким образом, начальный объём загрузки приложения уменьшится для всех пользователей, и только тем, кто отправляет изображения, потребуется загрузить этот дополнительный компонент.
Имейте в виду, что такой тип модуляризации требует больших усилий и, возможно, рефакторинга существующего кода вашего приложения, поэтому тщательно продумайте, какие функции вашего приложения больше всего выиграют от предоставления пользователям по запросу. Чтобы лучше понять оптимальные варианты использования и рекомендации по использованию функций по запросу, ознакомьтесь с рекомендациями по UX-дизайну для доставки по запросу .
Если вы хотите постепенно модулировать функции приложения с течением времени, не включая расширенные параметры доставки, такие как доставка по требованию, вместо этого настройте доставку во время установки .
Эта страница поможет вам добавить модуль функций в проект вашего приложения и настроить его для доставки по требованию. Прежде чем начать, убедитесь, что вы используете Android Studio 3.5 или более позднюю версию и Android Gradle Plugin 3.5.0 или более позднюю версию.
Настройте новый модуль для доставки по требованию
Самый простой способ создать новый функциональный модуль — использовать Android Studio 3.5 или более поздней версии. Поскольку функциональные модули имеют неотъемлемую зависимость от базового модуля приложения, их можно добавлять только в существующие проекты приложений.
Чтобы добавить модуль функций в проект вашего приложения с помощью Android Studio, выполните следующие действия.
- Если вы еще этого не сделали, откройте проект своего приложения в IDE.
- В строке меню выберите Файл > Создать > Новый модуль .
- В диалоговом окне «Создать новый модуль» выберите «Модуль динамических функций» и нажмите «Далее» .
- В разделе «Настройка нового модуля» выполните следующие действия:
- В раскрывающемся меню выберите базовый модуль приложения для вашего проекта.
- Укажите имя модуля . IDE использует это имя для идентификации модуля как подпроекта Gradle в файле настроек Gradle . При сборке пакета приложений Gradle использует последний элемент имени подпроекта для внедрения атрибута
<manifest split>
в манифест функционального модуля . - Укажите имя пакета модуля. По умолчанию Android Studio предлагает имя пакета, которое объединяет имя корневого пакета базового модуля и имя модуля, указанное вами на предыдущем шаге.
- Выберите минимальный уровень API, который должен поддерживать модуль. Это значение должно совпадать с уровнем API базового модуля.
- Нажмите кнопку «Далее» .
В разделе «Параметры загрузки модуля» выполните следующее:
Укажите название модуля длиной до 50 символов. Платформа использует это название для идентификации модуля пользователям, например, при подтверждении того, хочет ли пользователь загрузить модуль. Поэтому базовый модуль вашего приложения должен включать название модуля в виде строкового ресурса , который можно перевести. При создании модуля в Android Studio среда IDE добавляет строковый ресурс в базовый модуль и добавляет следующую запись в манифест функционального модуля:
<dist:module ... dist:title="@string/feature_title"> </dist:module>
В раскрывающемся меню «Включение при установке» выберите «Не включать модуль при установке» . Android Studio добавляет в манифест модуля следующее, чтобы отразить ваш выбор:
<dist:module ... > <dist:delivery> <dist:on-demand/> </dist:delivery> </dist:module>
Установите флажок рядом с опцией «Объединение», если хотите, чтобы этот модуль был доступен на устройствах под управлением Android 4.4 (API уровня 20) и ниже и включался в состав multi-APK. Это означает, что вы можете включить функцию «По требованию» для этого модуля и отключить «Объединение», чтобы исключить его из числа устройств, не поддерживающих загрузку и установку раздельных APK. Android Studio добавляет в манифест модуля следующее, чтобы отразить ваш выбор:
<dist:module ...> <dist:fusing dist:include="true | false" /> </dist:module>
Нажмите кнопку Готово .
После того, как Android Studio завершит создание модуля, проверьте его содержимое на панели «Проект» (выберите «Вид» > «Окна инструментов» > «Проект» в строке меню). Код, ресурсы и организация по умолчанию должны быть аналогичны коду, ресурсам и организации стандартного модуля приложения.
Далее вам необходимо реализовать функционал установки по требованию с использованием библиотеки Play Feature Delivery.
Включите библиотеку доставки функций Play в свой проект
Прежде чем начать, вам необходимо добавить в свой проект библиотеку 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 -> ... }
Ява
// Creates an instance of SplitInstallManager. SplitInstallManager splitInstallManager = SplitInstallManagerFactory.create(context); // Creates a request to install a module. SplitInstallRequest request = SplitInstallRequest .newBuilder() // You can download multiple on demand modules per // request by invoking the following method for each // module you want to install. .addModule("pictureMessages") .addModule("promotionalFilters") .build(); splitInstallManager // Submits the request to install the module through the // asynchronous startInstall() task. Your app needs to be // in the foreground to submit the request. .startInstall(request) // You should also be able to gracefully handle // request state changes and errors. To learn more, go to // the section about how to Monitor the request state. .addOnSuccessListener(sessionId -> { ... }) .addOnFailureListener(exception -> { ... });
Когда ваше приложение запрашивает модуль по запросу, библиотека Play Feature Delivery использует стратегию «запустил и забыл». То есть, она отправляет запрос на загрузку модуля на платформу, но не отслеживает, была ли установка успешной. Чтобы продолжить путь пользователя после установки или корректно обрабатывать ошибки, обязательно отслеживайте состояние запроса .
Примечание: Вы можете запросить модуль функции, который уже установлен на устройстве. API мгновенно считает запрос выполненным, если обнаруживает, что модуль уже установлен. Кроме того, после установки модуля Google Play автоматически обновляет его. То есть, при загрузке новой версии пакета приложений платформа обновляет все установленные APK-файлы, относящиеся к вашему приложению. Подробнее см. в разделе Управление обновлениями приложений .
Чтобы получить доступ к коду и ресурсам модуля, в вашем приложении необходимо включить SplitCompat . Обратите внимание, что SplitCompat не требуется для приложений Android Instant Apps.
Отложить установку модулей по требованию
Если вам не нужно, чтобы приложение немедленно загружало и устанавливало модуль по запросу, вы можете отложить установку до тех пор, пока приложение работает в фоновом режиме. Например, если вы хотите предварительно загрузить рекламные материалы для последующего запуска приложения.
Вы можете указать модуль для последующей загрузки с помощью метода 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. Рекомендации по корректному решению таких ситуаций с точки зрения пользователя см. в наших рекомендациях по 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 | Запрос отклонен, поскольку в данный момент выполняется загрузка как минимум одного существующего запроса. | Проверьте, есть ли какие-либо запросы, которые все еще загружаются, как показано в примере выше. |
МОДУЛЬ_НЕДОСТУПЕН | Google Play не может найти запрошенный модуль на основе текущей установленной версии приложения, устройства и учетной записи Google Play пользователя. | Если у пользователя нет доступа к модулю, сообщите ему об этом. |
НЕВЕРНЫЙ_ЗАПРОС | Google Play получил запрос, но запрос недействителен. | Убедитесь, что информация, указанная в запросе, является полной и точной. |
СЕАНС_НЕ_НАЙДЕН | Сеанс с указанным идентификатором не найден. | Если вы пытаетесь отслеживать состояние запроса по его идентификатору сеанса, убедитесь, что идентификатор сеанса правильный. |
API_НЕ_ДОСТУПНО | Библиотека доставки функций Play не поддерживается на текущем устройстве. Это означает, что устройство не может загружать и устанавливать функции по запросу. | Для устройств под управлением Android 4.4 (уровень API 20) и ниже необходимо включить функциональные модули во время установки с помощью свойства dist:fusing в манифесте. Подробнее см. в манифесте функциональных модулей . |
СЕТЕВАЯ_ОШИБКА | Запрос не был выполнен из-за сетевой ошибки. | Предложить пользователю либо установить сетевое соединение, либо перейти на другую сеть. |
ДОСТУП ЗАПРЕЩЕН | Приложение не может зарегистрировать запрос из-за недостатка разрешений. | Обычно это происходит, когда приложение работает в фоновом режиме. Повторите запрос, когда приложение вернётся на передний план. |
НЕСОВМЕСТИМОСТЬ_С_СУЩЕСТВУЮЩИМ_СЕАНСОМ | Запрос содержит один или несколько модулей, которые уже были запрошены, но еще не установлены. | Либо создайте новый запрос, не включающий модули, которые ваше приложение уже запросило, либо дождитесь завершения установки всех запрошенных в данный момент модулей, прежде чем повторить запрос. Помните, что запрос модуля, который уже установлен, не приводит к возникновению ошибки. |
SERVICE_DIED | Служба, ответственная за обработку запроса, прекратила работу. | Повторите запрос. Ваш |
НЕДОСТАТОЧНОЕ_ХРАНИЛИЩЕ | На устройстве недостаточно свободной памяти для установки модуля функции. | Уведомить пользователя о том, что у него недостаточно памяти для установки этой функции. |
SPLITCOMPAT_VERIFICATION_ERROR, SPLITCOMPAT_EMULATION_ERROR, SPLITCOMPAT_COPY_ERROR | SplitCompat не удалось загрузить модуль функций. | Эти ошибки должны автоматически устраниться после следующего перезапуска приложения. |
PLAY_STORE_NOT_FOUND | Приложение Play Store не установлено на устройстве. | Сообщите пользователю, что для загрузки этой функции требуется приложение Play Store. |
APP_NOT_WNED | Приложение не установлено в 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) или выше, вам необходимо использовать |
НЕУСПЕШНЫЙ | Запрос не был выполнен до установки модуля на устройство. | Предложите пользователю повторить запрос или отменить его. |
ОТМЕНА | Устройство находится в процессе отмены запроса. | Более подробную информацию см. в разделе об отмене запроса на установку . |
ОТМЕНЕНО | Запрос был отменен. |
Получить подтверждение пользователя
В некоторых случаях 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 для вашего приложения вам также необходимо включить 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() { ... }
Ява
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 Instant Apps на Android 8.0 и выше
При запросе модуля по запросу для мгновенного приложения Android на 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++ из модуля, который устройство уже загрузило в 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”) ... } } } }
Ява
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 на уровне API Android 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 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)
Ява
// 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 после установки нового языка и подготовки к использованию. Для этого можно использовать метод 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:
- Запрос и мониторинг установки модулей.
- Обрабатывать ошибки установки.
- Для доступа к модулям используйте
SplitCompat
.
На этой странице описывается, как развернуть разделенные APK-файлы вашего приложения на тестовом устройстве, чтобы Play Feature Delivery автоматически использовал эти APK-файлы для имитации запроса, загрузки и установки модулей из Play Store.
Хотя вам не нужно вносить никаких изменений в логику вашего приложения, вам необходимо выполнить следующие требования:
- Загрузите и установите последнюю версию
bundletool
.bundletool
необходим для создания нового набора устанавливаемых APK-файлов из пакета вашего приложения.
Создайте набор APK
Если вы еще этого не сделали, создайте разделенные APK-файлы вашего приложения следующим образом:
- Создайте пакет приложений для своего приложения, используя один из следующих методов:
- Используйте Android Studio и плагин Android для Gradle для создания и подписания пакета Android App Bundle .
- Создайте свой пакет приложений из командной строки .
Используйте
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 Feature Delivery использует альтернативу SplitInstallManager
— FakeSplitInstallManager
— для запроса модуля. При использовании bundletool
с флагом --local-testing
для сборки набора APK-файлов и их развертывания на тестовом устройстве он включает метаданные, которые указывают библиотеке Play Feature Delivery автоматически переключать вызовы 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);