- Android App Bundle и Dynamic Feature Modules
- Немного теории
- Преимущества
- Недостатки
- Сборка и локальный запуск
- Android Dynamic Feature Modules : The Future
- What is Dynamic Delivery?
- Dynamic Delivery with split APKs
- What are Dynamic Feature Modules?
- Creating a dynamic feature module
- Dynamic Delivery в многомодульных проектах (часть 2)
- Bumble Brew
- Подготавливаем модуль Brew
- DynamicDeliveryContainer
- Конфигурация Dynamic Feature Module
- Конфигурация приложения
- Как я тесты сломал
- Итоги
Android App Bundle и Dynamic Feature Modules
App Bundle — новый формат публикации Android-приложения в Google Play. Благодаря App Bundle размер приложения становится меньше, и пользователи могут легче и быстрее его скачать. Если вы планируете опубликовать свое приложение в сторе, изучите этот формат (скоро он станет обязательным для всех) и посмотрите рекомендации от ведущего разработчика мобильного Поиска Яндекса Алексея Цветкова. Леша поделился своими впечатлениями о App Bundle: плюсами, минусами, параметрами установки. Еще он рассказал о создании и загрузке динамического модуля.
Немного теории
App Bundle представляет собой файл *.aab , внутри которого содержатся ресурсы, скомпилированный код, метаданные, список модулей, манифестов и динамических библиотек. На своей стороне мы больше мы не генерируем APK-файлы, они создаются на основе файла .aab локально с помощью инструмента bundletool , или это делает Google Play самостоятельно после загрузки .aab .
Что генерируется из .aab -файла:
- Base APK — критическая секция, базовая функциональность, без которой невозможна работа приложения. Тот минимум, который идет по дефолту на девайс пользователя, код и неразделимые ресурсы.
- Configuration APKs — автоматически генерируемая часть. Она генерируется в зависимости от abi/density/language, доступна из коробки. Здесь находятся ресурсы, зависимые от девайсов.
- Dynamic feature APKs — часть, которая конфигурируется и управляется полностью разработчиком (загрузка, установка). Мы можем выделить в приложении опциональную функциональность и дозагрузить ее по требованию позже, тем самым заметно уменьшив размер приложения и избавив пользователя от необходимости загружать невостребованный контент. Для старых девайсов (API fuse mode — все склеивается в один APK.
Преимущества
Загрузка фич по требованию. Эта возможность носит практически революционный характер. Раньше мы могли динамически доставлять некоторые бинарные ресурсы (графические объекты, словари и т. д.), но нормальной (и легальной) динамической доставки кода не было. Здесь у нас открылись новые возможности — в основном, это касается нативных библиотек, которые действительно могут много весить. Пользователь загружает приложение приемлемого размера и может расширять по требованию его функциональность.
Теперь мы загружаем в Google Play только один файл, не нужно делать кучу сплитов в различных измерениях. Это приятный бонус к первому пункту. Мы, конечно, могли сделать много сплитов в разных измерениях и автоматизировать их загрузку в Google Play, но теперь эту обязанность берет на себя Google Play, а мы собираем один файл и живем счастливо.
Недостатки
Подпись приложения. Так как Google теперь генерирует APK-файлы на своей стороне, соответственно, ему нужно подписывать их нашим ключом. Поэтому нам нужно передать ключи для подписи APK-файлов. Сам .aab -файл тоже подписывается. Такой ключ называется ключом загрузки. В этом смысле для нас воркфлоу не сильно меняется — мы так же собираем файл, подписываем его ключом загрузки и отправляем в Google Play.
Трудности с локальным тестированием. Мы можем проверить только одно из бинарных состояний — фича установлена или отсутствует. Чтобы протестировать эти процессы, bundle нужно загрузить в Google Play (например, в internal track или alpha channel).
Недавно Google выкатил возможность локального тестирования.
Сборка и локальный запуск
В общем случае генерация .aab -файла выполняется командой
Как и в случае с apk , aab -файл нужно подписывать ключом загрузки. При команде bundleDebug приложение по аналогии с apk подписывается дебажным ключом. Чтобы локально потестировать получившийся результат, нам понадобится тулза bundletool.
Bundletool генерирует из aab- файла набор apk -файлов и складывает их в контейнер с расширением .apks (обычный zip-архив).
Сгенерированные apk должны быть подписаны. В этом случае bundletool попытается подписать apk-файлы дебажным ключом, но можно предоставить альтернативный вариант, например:
bundletool build-apks —bundle=./my.aab —output=./my.apks
bundletool build-apks —bundle=./my.aab —output=./my.apks —ks=./keystore.jks —ks-pass=file:/MyApp/keystore.pwd —ks-key-alias=MyKeyAlias —key-pass=file:/MyApp/key.pwd
Установка на девайс
bundletool install-apks —apks=./my.apks
Пример содержимого файла APKs
Источник
Android Dynamic Feature Modules : The Future
In Google I/O 2018, a new publishing format was introduced by Google known as Android App Bundle(.aab). A few out of many benefits of using app bundle are dynamic delivery, automatic multi-APK distribution, smaller APK size and dynamic feature modules.
Note : For more details about App Bundle, you can refer this blog .
What is Dynamic Delivery?
Google Play uses your app bundle to generate and serve optimized APKs for each user’s device configuration, so they download only the code and resources they need to run your app. For example, a user should not get x86 libs if the device architecture is armeabi. Also, users should not get other resources like strings and drawables they are not using.
You can also convert your independent modules/features (without these modules your app is functional) into dynamic feature modules which can be downloaded later. Through Dynamic Delivery, users can then download and install app’s dynamic features on demand after they’ve already installed the base APK of your app. As a result, the initial download size of your app is less and users don’t need to have unused code/feature in their devices.
Dynamic Delivery with split APKs
Split APKs are very similar to regular APKs — they include compiled DEX bytecode, resources, and an Android manifest. However, the Android platform is able to treat multiple installed split APKs as a single app. The benefit of split APKs is the ability to break up a monolithic APK into smaller, discrete packages that are installed on a user’s device as required.
- Base APK : This APK contains code and resources that all other split APKs can access and provides the basic functionality for your app. When a user requests to download your app, this APK is downloaded and installed first. That’s because only the base APK’s manifest contains a full declaration of your app’s services, content-providers, permissions, platform version requirements, and dependencies on system features.
- Configuration APKs : Each of these APKs includes native libraries and resources for a specific screen density, CPU architecture, or language. When a user downloads your app, their device downloads and installs only the configuration APKs that target their device. Each configuration APK is a dependency of either a base APK or dynamic feature APK. That is, they are downloaded and installed along with the APK they provide code and resources for.
- Dynamic Feature APKs : Each of these APKs contains code and resources for a feature of your app that is not required when your app is first installed. That is, using the Play Core Library, dynamic APKs may be installed on demand after the base APK is installed on the device to provide additional functionality to the user.
Note : Because devices running Android 4.4 (API level 19) and lower don’t support downloading and installing split APKs, Google Play instead serves those devices a single APK, called a multi-APK, that’s optimized for the device’s configuration.
What are Dynamic Feature Modules?
Dynamic feature modules allow you to separate certain features and resources from the base module of your app and include them in your app bundle. Users can download and install these modules later on demand after they’ve already installed the base APK of the app using Dynamic Delivery. For example, if your app has a custom camera feature you can make it a dynamic module and users can download and install the custom camera on demand.
Creating a dynamic feature module
Dynamic feature modules are organized just like regular app modules. They provide their own code, resources, and assets in the directories you would expect. To add a dynamic feature module to your app project using Android Studio(3.2 Canary 14+), proceed as follows:
Note : Before creating a dynamic module, you have to create you app bundle .
- Select File > New > New Module from the menu bar. In the Create New Module dialog, select Dynamic Feature Module and click Next.
Источник
Dynamic Delivery в многомодульных проектах (часть 2)
Привет! Меня зовут Юрий Влад, я Android-разработчик в компании Badoo и занимаюсь внедрением Dynamic Features в наши проекты.
Я уже рассказывал, что такое Dynamic Delivery и какой у него API. В этой статье я подробнее опишу, как я использовал Dynamic Delivery в нашем приложении и почему интеграция оказалась такой лёгкой. В результате мне удалось уменьшить вес приложения на полмегабайта для 99% наших пользователей, превратив доступную для жителей определённого региона функцию в загружаемый модуль.
Bumble Brew
Мы развиваем два ключевых продукта: приложения Badoo и Bumble. Я занимаюсь разработкой Bumble.
В последнее время мы экспериментируем в области взаимодействия с пользователями в офлайне и добавили новый экран с QR-кодом, который может быть считан где-нибудь (в ресторане, кафе, магазине). Выглядит он так:
Логики на нём практически никакой нет, зато есть очень красивый фон. Красивый аж на 547 KБ в максимальном размере. Из-за своей специфики этот экран будет использоваться очень маленьким количеством пользователей, проживающих в городе, где проводится офлайн-встреча. А полмегабайта на свои устройства получат все. В общем, идеальный кандидат для Dynamic Delivery.
Подготавливаем модуль Brew
Для создания экранов мы в Badoo используем архитектуру RIBs. Наши статьи о ней выйдут позже, а пока для простоты понимания мы будем рассматривать отдельный RIB как Fragment , ведь идея у них общая: оба они — самодостаточные элементы экрана, с UI или без, которые могут быть встроены в другие элементы или использованы самостоятельно.
Для любого модуля экрана, будь то Fragment или RIB , верно следующее утверждение: у него есть публичный API, который описывает взаимодействие с этим экраном извне, и внутренний API, который нужен для функционирования этого экрана. Обычно мы сегрегируем публичный и приватный API с помощью модификаторов доступа. Однако можно пойти дальше и разделить их на два разных модуля: :components:BrewScreen:Interface и :components:BrewScreen:Implementation .
В первом модуле мы описываем, как работать с экраном и какие у него зависимости.
Весь публичный API умещается в четыре интерфейса, с которыми и будет работать модуль приложения. Dependency объявляет зависимости, которые нужны для работы данного экрана. brewOutput выступает в качестве функции обратного вызова для получения результатов с данного экрана, uiScreen — информация о том, что и как нужно отобразить на экране (экран не делает сетевой запрос сам, а получает результат выполнения сетевого запроса извне), hotpanelTracker — для аналитики. Customisation позволяет изменить внешний вид экрана, заменив, например, логотип и фон на брендированные. Такой подход позволяет намного легче адаптировать уже существующие экраны для других приложений. Output задаёт результаты выполнения экрана. В нашем случае экран можно только закрыть. Но в будущем может появиться новый тип события, например «открыть какой-то другой экран в приложении».
Во втором же модуле мы реализуем всю логику и UI экрана.
Все классы этого модуля — internal , кроме BrewBuilder . BrewBuilder — фабрика, которая создаст экземпляр RIB , который мы сможем использовать в дальнейшем.
Наличие такой фабрики крайне важно для Dynamic Delivery, ведь её очень легко использовать через рефлексию. Все зависимости определены в одном интерфейсе, который будет доступен без рефлексии, а значит, его можно реализовать внутри модуля приложения. Сам процесс создания тоже требует вызова только с одним, заранее известным, параметром.
DynamicDeliveryContainer
Поскольку все классы, унаследованные от RibBuilder , реализуют единый интерфейс Builder , можно создать обобщённый RIB -контейнер для всех динамически загружаемых экранов.
Реализация DynamicDeliveryContainer делает следующее:
- Спрашивает у DynamicDeliveryFeatureDataSource , реализация которого приведена в первой части, установлен ли модуль childRibConfiguration.moduleName .
- Если модуль установлен, то вызывает childRibConfiguration.build(bundle) и размещает созданный RIB внутри себя.
- Если модуль не установлен, то запрашивает установку модуля childRibConfiguration.moduleName и показывает какой-нибудь красивый UI.
- Когда модуль установлен, заменяет красивый UI на загруженный RIB .
Абсолютно такую же логику можно реализовать с помощью фрагментов, ведь они также поддерживают размещение дочерних фрагментов внутри себя.
Конфигурация Dynamic Feature Module
Dynamic Feature Module является обычным Gradle-модулем. Рассмотрим конфигурацию на нашем примере:
Android Gradle plugin содержит новый плагин для таких модулей — com.android.dynamic-feature . Что важно, его использование практически не накладывает ограничений: вы можете спокойно переиспользовать скрипты настройки ваших модулей ( ProjectHelper.configureAndroidLibraryProject ).
Документация утверждает, что versionCode и versionName нужно не указывать, так как эти значения автоматически подтянутся из модуля приложения. Да, это так, вот только они могут быть закешированы. Я столкнулся с тем, что проект перестал собираться после изменения этих значений в модуле приложения. В итоге я решил вручную ставить версию, переиспользовав gradle.properties -файл приложения.
Зависимости, которые вы укажете, будут корректно обработаны плагином. В модуль будут включены только те, которых нет в основном модуле. В нашем случае это :components:BrewScreen:Implementation .
На этапе проверок CI я обнаружил, что androidTest не то что запуститься — даже собраться не может. По какой-то причине при попытке собрать модуль для тестов он вёл себя не как Dynamic Feature Module, а как обычный. Из-за этого возникало много ошибок на этапе слияния манифестов и ресурсов. Все тесты лежат либо в модуле приложения, либо в модуле экрана, а это значит, что даже пытаться запустить их не было смысла. Поэтому я отключил все задачи, связанные с androidTest .
Манифест для такого модуля выглядит следующим образом:
В нём мы настраиваем поведение модуля. В данном случае модуль не является Instant App, будет установлен по требованию ( on-demand ) и имеет полностью локализованное имя @string/dynamic.delivery.feature.brew . Последнее важно, так как это имя будет отображено в диалоге Google Play.
В данном случае локализованное имя модуля — Brew и оно используется в именительном падеже во всех языках, размер модуля был увеличен для теста
Также вы можете настроить установку этого модуля в тот момент, когда устанавливается само приложение. Это может быть полезно, когда вы делаете из экранов регистрации Dynamic Feature Module, чтобы после прохождения пользователем регистрации их удалить. dist:fusing отвечает за включение этого модуля в APK-файл для устройств на Android 4.4 и ниже, но такие мы уже не поддерживаем.
Android Gradle plugin сам заполнит поле split в манифесте именем Gradle-модуля из project.name . Именно это имя нужно использовать для загрузки модуля. Вы не можете изменить это имя, не изменив имя модуля.
Подробнее про все возможности вы можете прочитать в документации.
Итоговая структура Dynamic Feature Module получилась такой:
BrewCustomisationProvider отвечает за идентификатор картинки bg_brew.webp . Этот BrewCustomisationProvider мы создадим через рефлексию и используем при создании Dependency . Поскольку мы будем создавать Dependency после установки модуля, никаких проблем у нас не возникнет.
Предупреждение: при работе с Dynamic Feature Module следите за ресурсами. Те, которые находятся внутри модуля, вы должны получать через R -класс этого модуля. Те, которые лежат в модуле приложения, — через R -класса приложения (как я это делаю в случае с ic_bumble_logo ). В противном случае вы будете получать ResourceNotFoundException . Оба варианта использования доступны из кода, так как aapt сгенерирует R -класс вместе с идентификаторами всех ресурсов из модуля приложения.
Конфигурация приложения
В первую очередь сконфигурируем модуль приложения.
В BaseAppModuleExtension появилось новое свойство dynamicFeatures , в котором необходимо перечислить все пути до подключаемых Dynamic Feature Modules.
К модулю приложения также подключим в качестве зависимостей DynamicDeliveryContainer , который осуществит загрузку модуля и отобразит его на экране, и BrewScreen:Interface , в котором объявлены только интерфейсы для работы с экраном.
Создадим класс, в котором будем хранить все константы для работы с Dynamic Feature Module. К ним относятся имя класса фабрики создания RIB , имя класса кастомизации интерфейса и имя модуля (как в Gradle).
Поскольку эти классы используются через рефлексию, добавим их в ProGuard.
Осталось создать экземпляр DynamicDeliveryContainer , передать в него нужные параметры и подключить к Activity .
Приведённый выше код можно сделать менее громоздким, вынеся создание зависимостей в Dagger. Но описание Dagger-компонентов и модулей сделало бы наш код более запутанным, так что для простоты я создаю все зависимости на месте.
Bumble Brew доступен не всем пользователям, а только для тем, кто проживает в городе проведения мероприятия. Такие пользователи включаются в специальную группу. Список групп, в которых состоит пользователь, приложение получает при первом подключении к серверу, а значит, именно в этот момент мы можем сказать, будет показываться кнопка открытия экрана Brew или нет, и в этот же момент мы можем запросить отложенную установку модуля с помощью SplitInstallManager.deferredInstall .
Как я тесты сломал
В Badoo мы используем полноценные Е2Е-тесты. Тестовый инструментарий (в моём случае это Appium) устанавливает приложение и симулирует нажатия пользователя на экран. При этом само приложение использует актуальную версию бэкенда. Для установки приложения мы используем следующую функцию:
Для тестирования мы используем App Bundle , из которого создаём APKS и устанавливаем на устройство. Установка APKS производится с помощью команды install-apks . И если вы тоже используете bundletool в своих Е2Е-тестах, то не забудьте добавить параметр —modules _ALL_ , чтобы установить все Dynamic Features сразу. У нас такого параметра не было, поэтому все тесты для Brew начали падать.
Как я упоминал выше, если приложение не установлено из Google Play, то оно не может устанавливать модули. Либо они все установлены сразу, либо нет. Поэтому подход с —modules _ALL_ не охватывает сценарий установки Brew модуля. В данный момент мы используем Internal App Sharing для тестирования Dynamic Features.
Однако недавно Google выпустила новую версию com.google.android.play:core , в которой появился FakeSplitInstallManagerFactory. По функционалу он не полностью аналогичен SplitInstallManager , но может повысить качество автоматизированного тестирования. Для того чтобы задать его поведение, доступны два параметра:
- setShouldNetworkError , после указания которого любая установка будет завершаться с ошибкой.
- modulesDirectory для указания папки на устройстве, откуда нужно устанавливать запрашиваемые модули. Сами же модули копируются в эту папку в виде APK-файлов.
Получить необходимые APK-файлы для установки можно следующей командой:
Полученные APK-файлы нужно передать на устройство через adb и указать папку в FakeSplitInstallManagerFactory .
Хоть этот подход и приближен к ручному тестированию с помощью Internal App Sharing, для его использования понадобится отдельная сборка приложения, использующая FakeSplitInstallManagerFactory . Также придётся внести изменения в тестовый инструментарий. Так что, наверное, игра стоит свеч только тогда, когда динамических модулей становится больше.
Корректность рефлексии же можно проверять обычными юнит- и интеграционными тестами. В моем случае есть Е2Е- и интеграционный тесты, которые просто переходят с главного экрана на экран Brew.
Итоги
Представленная Google технология Dynamic Delivery позволяет загружать и удалять модули прямо во время работы приложения.
Если у вас многомодульный проект, то, скорее всего, все модули имеют чёткую сегрегацию на публичный и приватный API. Создав для каждого из них отдельные модули, становится довольно просто использовать Dynamic Delivery. При такой структуре рефлексию необходимо использовать только для создания экземпляров классов публичного API.
Когда все экраны приложения имеют схожую архитектуру и чётко обозначенные зависимости, для загрузки и отображения Dynamic Feature Module можно создать универсальный контейнер. С его помощью вы очень быстро сконвертируете в Dynamic Features те экраны приложения, которые редко используются юзерами, и таким образом ещё больше уменьшите вес приложения.
Источник