Часть 1: Динамическая доставка в многомодульных проектах в Bumble

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

В части 1 этой статьи я более подробно исследую динамическую доставку и ее API, в частности, как загружать и удалять модули. В части 2 на примере я расскажу, как я использовал динамическую доставку в нашем приложении и уменьшил размер приложения, сэкономив пол мегабайта места.

Итак, приступим!

Модули для динамической доставки

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

  1. функции, относящиеся к А / Б тестам и группам пользователей. Некоторые функции могут быть доступны только в определенных регионах, и без динамической доставки на устройствах всех других пользователей они просто мертвый груз.
  2. специфические функции, которые нужны не всем пользователям. Классическим примером может служить модуль с камерой для распознавания номера банковской карты.
  3. функции, которые больше не доступны после выполнения пользователем определенных действий. Это могут быть, например, экраны регистрации, которые можно удалить после завершения регистрации и переустановить позже, если пользователь решит зарегистрировать другую учетную запись.

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

Модули с перечисленными выше функциями могут содержать код и ресурсы любого типа. После установки классы загружаются в ClassLoader и могут использоваться. Вы сможете получить доступ к ресурсам из установленного модуля. Однако есть одно «но» ...

Модуль динамических функций

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

ClassNotFoundException будет возвращено. В таком случае отражение позволяет:

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

Вот как это выглядит с точки зрения структуры модулей в Gradle:

Модули в первой строке (:about и другие) - это модули динамических функций. Они зависят от базового модуля приложения и могут легко использовать его код и ресурсы. Модуль приложения находится во второй строке, а его зависимости - в третьей.

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

SplitInstallManager

Для начала давайте посмотрим, как устанавливать модули. Для этого мы используем SplitInstallManager, который является частью библиотеки com.google.android.play:core. Возможно, вы уже знакомы с этим из Обновлений в приложении и MissingSplitsManager.

Вот как работать с модулями:

  1. с помощью SplitInstallManager.installedModules убедитесь, что модуль еще не установлен.
  2. если модуль не установлен, запросить установку с помощью SplitInstallRequest, указав его имя.
  3. отслеживать процесс установки; показать пользователю модальный пользовательский интерфейс для загрузки, если пользователь ожидает.
  4. если модуль успешно установлен, начните использовать его через рефлексию. Если произошла ошибка, покажите ее.

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

Проверка установки модуля

Запросить установку

splitInstallManager.startInstall вернет Task<Int>, но не из пакета com.google.android.gms:play-services-tasks, для которого в вашем случае, скорее всего, уже были написаны расширения Kotlin Extensions, а скорее свое собственное. Их API полностью совпадает, но названия пакетов разные. Идентификатор сеанса установки возвращается в обратном вызове addOnSuccessListener. Более того, тот же самый возвращается, если вы запрашиваете установку несколько раз, так что не бойтесь сделать это. Есть только одно ограничение: если вы укажете загрузку нескольких модулей одновременно через SplitInstallRequest.addModule(…).addModule(…), то при попытке запросить установку только одного из них во втором или последующем случае будет возвращена ошибка INCOMPATIBLE_WITH_EXISTING_SESSION. Если ошибка произошла до принятия сеанса или во время установки, ошибка будет возвращена в addOnFailureListener.

Ход установки

Обновления состояния установки будут доступны в SplitInstallStateUpdatedListener. Здесь доступны обновления всех сеансов, и нам нужно отфильтровать их самостоятельно на основе идентификатора сеанса. В SplitInstallSessionState нам доступны:

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

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

Если размер модуля превышает 10 МБ, вам необходимо запросить у пользователя подтверждение загрузки. В SplitInstallSessionState возвращается особое состояние REQUIRES_USER_CONFIRMATION. Это будет состояние установки, пока вы не позвоните splitInstallManager.startConfirmationDialogForResult(Activity, SplitInstallSessionState, Int). Этот вызов запустит Google Play через startActivityForResult с диалоговым окном подтверждения установки. Если пользователь нажимает кнопку «Загрузить», установка продолжится, и вам не нужно ничего делать. Если они нажмут кнопку «Отмена», установка завершится с состоянием CANCELED.

Установка

Для поддержки загрузки классов и ресурсов в приложении необходимо использовать SplitCompat.

Версия приложения извлечет classes.dex из загруженных файлов APK и загрузит их в ClassLoader, а также вызовет context.getAssets().addAssetPath(String) с файлом APK загруженного модуля. Версия Activity просто добавит путь к AssetManager. Может показаться ненужным вызывать installActivity, если вы используете Context приложения, но невыполнение этого может вызвать проблемы с конфигурацией Activity, которая в этом случае будет проигнорирована.

Отложенная установка

Вы можете запросить сервисы Google Play для установки модуля в какой-то момент в будущем. Официальная документация описывает время «в какой-то момент в будущем» как «оптимальное время, когда приложение работает в фоновом режиме». На практике модуль будет загружен, когда ваше приложение не запущено и когда Google Play устанавливает обновления для вашего приложения или других приложений.

Запросить установку в фоновом режиме очень просто:

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

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

Собираем все вместе

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

В случае сборок без выпуска установленные модули будут пустыми. Итак, мы добавляем проверку того, существует ли класс, который мы собираемся использовать через отражение. Мы используем именно эту перезагрузку Class.forName, чтобы не инициализировать статические поля класса и выполнить работу, которую можно было бы отложить на потом.

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

К сожалению, мы не можем полностью отказаться от использования SplitInstallSessionState в RequiresConfirmation, поскольку это необходимо для вызова splitInstallManager.startConfirmationDialogForResult. Однако нам, кажется, нужен только идентификатор сеанса. Я надеюсь, что это изменится в будущем.

Нам также нужна функция, которая загрузит данный модуль и будет отслеживать прогресс. В Bumble мы используем реактивный подход, поэтому мы вернем Observable<DynamicDeliveryProgress>. Как только модуль будет установлен, мы завершим observable.

Со стороны пользовательского интерфейса не забывайте обрабатывать состояние DynamicDeliveryProgress.RequiresConfirmation. После завершения установки вам нужно будет снова позвонить SplitCompat.installActivity(this), чтобы загрузить ресурсы из загруженных файлов APK.

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

  1. снова подписывается на обновления состояния через splitInstallManager.registerListener.
  2. находит сеанс в splitInstallManager.getSessionStates, в котором устанавливаются запрошенные модули.
  3. ожидает завершения установки и отправляет запрос на повторный вызов load(…).

Заключение

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

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

Следует отметить еще два момента:

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

Во второй части статьи я расскажу, как я использовал Dynamic Delivery для одного из проектов Bumble.