Когда ввязываешься в Early Access Program, никогда заранее не знаешь, что получится в итоге. Конечно, надеешься, что технология взлетит, а твое приложение опередит рынок и получит порцию пиара на Google I/O. И это неплохая мотивация, чтобы на начальном этапе вместо документации читать исходники, которые к тому же надо качать из секретного архива.
На AppsConf Евгений Сатуров показал, во что может вылиться участие в Early Access на примере разработки приложения с мгновенным запуском, по дороге поясняя все особенности Google Play Instant. В расшифровке его доклада разберёмся, откуда пошли Android App Bundles, при чём тут вообще Dynamic Delivery, познакомимся с новыми Gradle-плагинами, и узнаем, как быть с неожиданностями, которые приготовили для нас разработчики SDK.
О спикере: Евгений Сатуров (saturovv) работает в компании Surf, которая занимается заказной Android и iOS-разработкой, а с недавнего времени еще и разработкой на Flutter. Евгений Flutter-энтузиаст и основатель FlutterDevPodcast.
Краткий экскурс в историю
![Google Play Instant. Рефакторинг длиною в жизнь - 1 Google Play Instant. Рефакторинг длиною в жизнь - 1](https://www.pvsm.ru/images/2019/10/08/Google-Play-Instant-refaktoring-dlinoyu-v-jizn.png)
Два года назад к нам в Surf пришел замечательный парень и говорит: «У нас есть технология, о которой пока мало, кто знает. А у вас есть заказчики и интересные кейсы. Давайте, мы дадим вам нашу технологию, вы ее куда-нибудь интегрируете, и получится классный симбиоз. Мы пропиарим это на Google I/O и всем будет хорошо».
На самом же деле, работа с Early Access Program — это кот в мешке. Приходится работать с сырым кодом, который, понятное дело, не всегда работает так, как задумано и описано. В нашем случае:
- Все артефакты поставлялись в виде ZIP-архива, который надо было раз в несколько дней выкачивать из суперсекретного хранилища и обновлять на локальной машине только вручную.
- Работать можно только в «канарейке».
- Документации почти нет, только разрозненные неструктурированные Google Docs, чаще похожие на поток мыслей разработчиков.
- Деплоиться в прод, естественно, нельзя —это возможно только после того, как технология попадет в публичный релиз. То есть весь деплой только в альфа-треке.
- Есть вероятность, что после публичного релиза, обнаружится, что SDK был полностью переписан без поддержки обратной совместимости. Вы получаете совсем другие интерфейсы и API — вам надо снова все переделывать.
Это краткий пересказ того, как мы участвовали в Early Access Program.
Все это стало возможным благодаря нашему постоянному партнеру — компании Лабиринт — крупнейшему интернет-магазину книг и канцтоваров в России. Они включились в проект, даже не будучи уверенными, будет ли какой-то бенефит в итоге. И тому, что в 2017 году Surf, как студия, была частью программы Программе Google Certified Agency. К сожалению, программу закрыли в этом году.
Android Instant Apps
В 2017 для того чтобы приложение запускалось без установки, оно должно было быть распилено на модули правильным образом.
У нас появились новые Gradle-плагины, которые генерировали соответствующие артефакты:
- Instant App Module — application-плагин, который генерировал ZIP-архив с APK, по одному на каждый feature-модуль приложения;
- Installable App Module — генерировал APK.
Появилась двухуровневая иерархия из feature modules. Базовый feature module всегда мог быть только один и содержал в себе весь базовый код, доступный для всех фич, ресурсы, зависимости и т.д. Feature module верхнего уровня содержал реализацию конкретных экранов.
Но это было долго и дорого по ряду причин:
- Жёсткая модульная структура сама по себе — большое ограничение. Трудно представить, что какая-то крупная компания превратит свое годами создающееся и отлично работающее приложение в груду дымящихся модулей только ради того, чтобы поддержать новую технологию, которая еще неизвестно, взлетит или нет.
- Строгое ограничение по размеру сборки, то есть одна фича не должна была превышать 4 Мб. Это объективно мало, а иногда и вовсе недостижимый лимит, например, из-за тяжелых зависимостей или чего-то подобного.
- Обязательно использовать AppLinks, потому что это единственный способ попасть в Instant App. Пользователь переходил по ссылке из почты, мессенджера, поисковой выдачи и попадал на ваш экран через перехват ссылки.
- Наконец, большое количество технических ограничений. Сначала даже было нельзя использовать NDK. Нельзя отправлять push-уведомления, получать sensitive data, менять настройки устройства и запускать фоновые процессы. Services, Broadcast Receivers и Content Providers использовать было нельзя. Только Activity.
Основная миссия Instant App — это просто демонстрация UI и ничего больше. Рефакторинг был очень болезненным, и поэтому до сих пор эта история практически не встречается в продакшене.
Но справедливости ради, внедрение Instant App в Лабиринте дало 5% прироста к покупкам через мобильный клиент.
2019. Google Play Instant
Прошло два года, на дворе 2019 — Instant Apps все еще существует, но не как самостоятельная технология. Она до сих пор очень редко встречается в продакшене, я не искал специально, но знаю только несколько примеров: это Sports.ru, Vimeo. Вряд ли Google рассчитывал на такой результат, когда анонсировал эту технологию.
Теперь Instant App называется иначе — Google Play Instant. Смена названия помогла отсеять неактуальную документацию. Если видите Android Instant App, сразу понятно, что это уже неактуально.
Кроме названия сменилось и все остальное, в том числе модульная структура. Требования стали более лояльными. Технология совершенно по-другому интегрируется в проект и не требует столь болезненного рефакторинга, что, безусловно, хорошо.
Но что менее очевидно и на мой взгляд гораздо более важно — эта технология, так и оставшись очень нишевой и редкой, стала прародителем целого семейства технологий, в составе которого теперь поставляется.
Изначально Google позиционировал Instant App как технологию, которая привлекает людей в бизнес, не в приложения. Был design-guideline, который запрещал делать лэндинг-приложения с одной кнопкой «Скачать полное приложение». Но побочно была решена другая фундаментальная проблема, и подозреваю, это произошло случайно.
В феврале этого года свет увидело замечательное устройство Samsung Galaxy S10+ с 1 Тб памяти на борту. Только вдумайтесь — один терабайт! Зачем столько нужно?!
Согласно официальной статистике Google, за последние 7 лет средний размер APK увеличился в 5,5 раз.
Размер сборки действительно имеет значение, исследование этой статистики показывает:
- каждые дополнительные 6 Мб сборки снижают конверсию в установке на 1%;
- 70% пользователей проверяют размер приложения перед скачиванием;
- 50% пользователей интересует, сколько места приложение займёт на устройстве после установки.
Это еще важнее, если ваша целевая аудитория — это люди возрастные, или с не очень высоким достатком, или это развивающиеся рынки.
Последние все сложнее игнорировать, потому что в 2018 году именно Индия показала невероятный прирост по установкам.
Обратите внимание, там совсем мало iOS, а суммарное количество установок Android-приложений превышает таковое в США, Бразилии и Индонезии вместе взятых.
Теперь понятно, какую проблему можно решить при помощи Google Play Instant. Можно переходить к непонятным терминам.
Android App Bundle
Android App Bundle — новый формат публикации приложения в Google Play. Внутри все не сильно отличается от APK: все те же самые dex-файлы, manifest, resources, assets и т.д. и т.п. Но есть еще Metadata, которая не попадает на устройство пользователя.
Metadata представлена тремя файлами: resources.pb, assets.pb, native.pb. Фактически это таблицы соответствия ресурсов, которые есть в сборке, и конфигурационных настроек устройств.
Android Dynamic Delivery
Все знают о существовании App Signing by Google Play. Но не все готовы хранить релизный ключ своего приложения в Google Play, потому что пути назад не будет. Слезть с App Signing by Google Play невозможно.
Один раз отдав релизный ключ в Google Play, вы потом никогда не сможете подписывать свое приложение как раньше, на своей стороне. Но в обмен на это вы можете пользоваться всеми преимуществами, которые дает Android App Bundle как формат. А процесс подписи сборки будет выглядеть отныне немножко иначе.
Вы все еще будете подписывать сборку перед деплоем в Google Play, но вы будете подписывать ее upload-ключом, который не является уникальным. Его можно отозвать из консоли, перевыпустить, если он скомпрометирован или утерян. Свой релизный ключ вы отдаете в консоль и прощаетесь с ним — Google теперь будет подписывать сборку за вас и клянется, что сохранит ваш ключ в безопасности.
Однако, если вы не отдадите релизный ключ в Google Play, то не сможете применить в своём проекте ничего из того, о чем дальше пойдет речь. Google закручивает гайки, и даже Instant App, не отдавая ключ, теперь задеплоить не получится.
На самом деле это совсем не смешно, потому что люди, которые купились на промоушен Instant App и влезли во всю эту историю, отрефакторили свои приложения, но по каким-то причинам не могут отдать свой ключ в Google Play (либо отдел безопасности категорически против, либо по другим объективным причинам) оказались в ситуации, когда они дальше не могут поддерживать это решение. Сотни часов работы фактически оказались выброшенными на помойку.
2014. В Android Lollipop появляется поддержка Split APK
Раз мы сегодня ностальгируем, вернемся еще раньше в прошлое — в 2014 год.
До сих пор помню, как на мой NEXUS 5, лучший телефон на свете, прилетела сборка Android Lollipop c невероятным material-дизайном, который выглядел просто бомбически. Но были изменения, которые остались для многих незамеченными — это поддержка Split APK.
Split APK — механизм, который позволяет разбивать приложение на маленькие APK и, устанавливая на одно устройство, заставлять их вести себя как единое приложение.
Запомните это, и пойдем дальше.
Android Dynamic Delivery — это новый формат дистрибуции приложений в Google Play.
Был у нас раньше APK, теперь появляется еще Android App Bundle, пока что как альтернатива. AAB выступает инкубатором-генератором этих самых Split APK. AAB выкатывает банч APK, которые дальше можно использовать как обычное приложение, установив их параллельно.
Разберемся, что это за APK.
Как минимум, это базовый APK, который играет ту же самую роль, что и в Instant App: это базовый код, базовые ресурсы и бизнес-логика, которая шарится между всеми фичами.
Кроме того появляются:
- APKs, в названии которых есть подозрительно похожие на модификаторы графических ресурсов префиксы (верхний ряд на схеме).
- Еще одно семейство APKs напоминает нам об архитектурах процессора.
- Локализационные APKs.
Года два назад, когда векторная графика была не так сильно распространена, многие приложения содержали в основном растровые ресурсы, которые нарезались под разные плотности пикселей на экранах. Сборка могла становиться достаточно увесистой. Пользователи скачивали все эти ресурсы, и большая их часть хранилась на устройстве мертвым грузом.
Теперь такой механизм позволяет получать пользователю только тот набор ресурсов, который нужен конкретно для его устройства. Человек приходит в Google Play, выбирает приложение, Google Play понимает характеристики устройства пользователя, и отдает тот набор Split APKs, который нужен — по одной APK из каждой категории.
Типы Dynamic Delivery APK:
- Один и только один Base APK.
- Набор Configuration APK максимум трех типов: res*x, assets*y, lib*z. Здесь: x — количество используемых ресурсных модификаторов; y — количество используемых типов архитектур; z — количество языковых локализаций. Если, например, в проекте не используется нативный код и нативные библиотеки, то категории, связанной с нативным кодом, не будет, и останется два APK.
- Неограниченное количество Dynamic Feature APK.
Dynamic Feature APK дальше разберем подробнее. Но для начала мы попрощаемся со Split-блоком.
Прощай, Split
Вы можете возразить, что и раньше можно было делать что-то подобное, вручную генерируя кучу APK только с нужным набором ресурсов, потом это все вручную деплоить в Google Play.
android {
splits {
density {
enable true
exclude "ldpi", "xxhdpi", "xxxhdpi"
compatibleScreens 'small', 'normal', 'large', 'xlarge'
}
}
}
Сама по себе эта затея достаточно сомнительная, а теперь все это просто игнорируется. Если вы собираете Android App Bundle, появляется блок Bundle, который позволяет отключать вручную разбиение проекта по одной из категорий.
android {
bundle {
language {
enableSplit = false
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
}
Можно указать, что приложение поддерживает, например, только русский или только английский язык, и пропустить этот этап при сборке.
Самые внимательные, наверное, сейчас думают о том, что же делать с preLollipops. Только в Android 5 появилась поддержка Split APK. Из этой ситуации надо как-то выходить, потому что Min SDK еще далеко не у всех 21-й.
Для preLollipops из ситуации выходят достаточно топорным, но единственно возможным образом. Google Play собирает Multi-APK для preLollipops, которые включают в себя всевозможные комбинации конфигурационных APK. APK в итоге один, но его вариантов очень много.
Android App Bundle меняет нашу жизнь
И очень существенно. Во-первых, собирать проект, может быть, стало значительно проще, особенно если раньше вы вручную собирали Split APK. Но по моим наблюдениям таких людей не очень много.
Во-вторых, вы больше не рискуете утратить или скомпрометировать релизный ключ. Не будет большой трагедии, если вы потеряете свой Upload Key, его можно отозвать и перевыпустить.
Не будем показывать пальцем — в топе российского Google Play есть приложения, у которых релизные ключи уже давно на 4PDA, все кастомные сборки подписаны релизным ключом, и в ближайшие лет пять с этим ничего не сделать. Остается ждать перехода на Signing V3, который появился только с 28 API.
Безусловное преимущество Android App Bundle: пользователи перестают расходовать трафик и место на диске на ненужные им ресурсы. Это очень повышает retention приложения.
Но если у вас вся графика в векторе, две локализации и нет нативных библиотек, то выгода будет микроскопическая.
Dynamic Feature Module
Dynamic Feature Module — это функциональный модуль, который не поставляется при установке приложения, а докачивающийся из Google Play и устанавливающийся только по требованию.
Такие модули встают на один уровень с базовым APK.
Важно, что каждый из этих feature module сам по себе тоже содержит набор конфигурационных Split APK. Соответственно, общее количество APK может увеличиться сверх всякой меры. Но это совершенно не ваша забота, этим занимается Google Play.
Области применения Dynamic Feature Modules:
Фичи, используемые очень малым процентом аудитории, но, тем не менее, важные для вашего продукта. Например, это контентное развлекательное приложение, и 95% его пользователей потребляют контент. Но есть очень маленький процент редакторов, которые контент генерируют. Для них есть крутой видеоредактор, который весит реально много, работает невероятно круто. Тогда не имеет смысла утяжелять сборку для всех и каждого, можно вынести эту фичу в Dynamic Feature Module, и предоставить её только тем, кому она нужна, докачивая ее уже потом.
Тяжёлые фичи, не относящиеся к основному сценарию использования приложения. Например, AR-навигация в картографическом сервисе. Любая AR-фича самое то, чтобы вынести ее в Dynamic Feature Module.
Фичи, которые должны быть доступны пользователям без установки самого приложения (например, выбор товара в каталоге и оформление заказа). Верно, это что-то очень подозрительно все напоминает.
Android Instant Apps — это теперь Instant-Enabled Dynamic Feature Module.
Таким образом получается, что есть два типа Dynamic Feature Modules:
- Обычные Dynamic Feature Modules — функциональные модули, загружаемые и устанавливаемые на устройство отдельно от основного приложения. Они живут там столько, сколько живет само приложение. Пока вы его не удалите, Dynamic Feature Module будет.
- Instant-Enabled Dynamic Feature Modules — функциональные модули, доступные для запуска без установки на устройство. Этот модуль живет ограниченное время.
Приложения второго вида в Google Play можно отличить по наличию кнопки «Попробовать». При нажатии на нее, подставляется URL по умолчанию и можно увидеть, как выглядит приложение хотя бы на примере одной главной фичи.
Такое часто можно встретить в разделе с играми. Это удобно и даже более применимо, чем к приложениям, потому что можно скачать маленький кусочек игры как демку и посмотреть, что это вообще такое, стоит ли тратить свой трафик и время.
Модульная структура
Я уже говорил, что модульная структура упростилась. Посмотрим, как именно.
Изначально у нас была достаточно монструозная структура. Среди прочего в ней были пустые модули, например, Instant App Module всегда был пустой, в нем не было ни кода, ни ресурсов, а только файл build.gradle и все.
Разработчики подумали, зачем тогда плодить лишние модули только для того, чтобы собирать артефакты определенного типа. И выпилили его, а функциональность перенесли в App Module.
Но потом они пошли еще дальше и подумали — зачем нужен Base Feature Module? От него одни проблемы, потому что мы инициализируем там все, а у него Application ID другой. Отсюда идут костыли, например, передача Application ID из App Module в Base Feature Module, и подстановка его как настоящего Application ID приложения, чтобы все трекалось в Crashlytics и т.д.
В итоге получилось так.
Оставили App Module, к нему сверху приделали Feature Modules — и всё!
Не передать мои эмоции, когда я об этом узнал. Ребята с большой сцены заявили, что у них есть классная технология: «Давайте все перепиливать приложения под эту структуру!» Но это не работа на 5 минут, а работа с большими последствиями.
В 2017 году иметь монолитное приложение было еще не стыдно. Лабиринт был именно таким, он тогда даже еще не вышел в публичный релиз, а был в бете. На момент, когда мы ввязались в Early Access Program, там уже было порядка 90 экранов. Мы потратили два дополнительных месяца на то, чтобы это все отрефакторить, протестировать и убедиться, что все действительно работает.
И после этого они говорят: «Мы перемудрили, можно гораздо проще сделать».
Но вернемся к прозе.
Конфигурация Gradle-файлов
Для того, чтобы поддержать новую конфигурацию, нам нужно сперва в файле build.gradle app-модуля в блоке Android перечислить все Dynamic Feature Modules:
// в build.gradle app-модуля
android {
dynamicFeatures = [":dynamic_feature", M:dynamic_feature2M]
}
После этого в build.gradle файле каждого Dynamic Feature Module прописать зависимость на app-модуль:
// в build.gradle каждого dynamic feature-модуля
dependencies {
implementation project(':app')
}
В принципе, ничего сложного. Но есть еще конфигурация манифестов.
Конфигурация манифестов
В манифесте app-модуля мы можем повесить флаг true, чтобы обозначить, что в этом приложении есть хотя бы одна Instant-Enabled Feature:
// в манифесте app-модуля
<dist:module dist:instant="true|false" />
Если этого флага не будет, нельзя будет задеплоиться в соответствующий трек в Google Play.
Кроме того, есть еще конфигурация манифеста каждого Dynamic Feature Module в отдельности, в котором больше настроек:
// в манифесте каждого dynamic feature-модуля
<dist:module
dist:instant="true|false" //помечаем Instant-Enabled модуль
dist:onDemand="false|true" // указываем, будет ли модуль включён в сборку при установке
dist:title="@string/feature_modulejnstall_name"> // именуем модуль
<dist:fusing dist:include="true |false" /> //указываем, будет ли модуль включён в APK для preLollipop
</dist:module>
Первые два флага немножко взаимоисключающие, потому что onDemand
— это обычная базовая фича, а instant
— это та самая Installable фича.
Title — техническое название модуля, по которому мы потом, захардкодив его в нашем приложении, будем выкачивать этот модуль из Google Play.
Параметр include
— параметр для preLollipops. Если установить его значение false, то пользователи preLollipops никогда эту фичу не увидят и не смогут ей воспользоваться.
Конфигурация Gradle-projects
Жизнь instantapp-плагина и feature-плагина была достаточно короткой, но яркой. Они просуществовали менее двух лет. С марта этого года они больше не поддерживаются.
Теперь остался только один Dynamic Feature Module, который мы и используем:
apply plugin: 'com.android.dynamic-feature'
Конфигурация проекта только в app-модуле
Важный момент: все настройки, касающиеся подписи (signing-конфигурация), сборки (ProGuard-конфигурация), versionCode и versionName нужно делать только в build.gradle app-модуля.
Иначе, они будут проигнорированы. Избегайте указания какого-либо из этих конфигурационных блоков в build.gradle-файлах dynamic feature-модулей.
Google Play Instant
Сейчас мы имеем следующее.
Требования к модульной структуре максимально упростились. Это действительно хорошая новость для тех, кто в это пока не ввязывался. Теперь можно попробовать, даже если у вас приложение со своей принципиальной структурой. Это совершенно не повлияет, вы можете просто приделать модуль сверху, и все будет хорошо работать.
Ограничения на размер сборки стали лояльнее. Если раньше было 4 Мб, то теперь:
- Dynamic Feature Modules вообще не ограничены по размеру;
- Instant-Enabled Dynamic Feature Modules могут занимать до 10 Мб.
Но теперь есть прогрессивная шкала.
Если ваша фича:
- больше 10 Мб, то извините;
- от 4 до 10 Мб — доступна по кнопке «Попробовать» из Google Play и все;
- меньше 4 МБ — доступны все средства привлечения пользователей в Instant-Enabled модуль (запуск из рекламы, по ссылке, из сообщений и т.д.).
Появился механизм подгрузки модулей — Play Core API. Мало кто знает, что раньше Instant App устанавливался через Chrome.
@Override
public boolean maybeLaunchInstantApp(Tab tab, String url, String referrerUrl,
boolean isIncomingRedirect) {
if (tab == null || tab.getWebContents() == null) return false;
InstantAppsHandler handler = InstantAppsHandler.getInstance();
Intent intent = tab.getTabRedirectHandler() != null
? tab.getTabRedirectHandler().getInitialIntent() : null;
if (isIncomingRedirect && intent != null &&
intent.getAction() == Intent.ACTION_VIEW) {
Intent resolvedIntent = new Intent(intent);
resolvedIntent.setData(Uri.parse(url));
return handler.handleIncomingIntent(getAvailableContext(), resolvedIntent,
ChromeLauncherActivity.isCustomTabIntent(resolvedIntent));
} else {
...
}
return false;
}
Это реальный кусок кода из Chrome под Android, который перехватывал ссылку на ваш Instant App, шел в Google Play и подставлял оттуда Instant Apps. Если Instant App обнаруживался, он вам каким-то образом открывал activity, из которой потом запускался Instant App.
Отсюда были огромные проблемы с раскаткой этой фичи. С Samsung это вообще отдельная история, я подозреваю, что их встроенный браузер имеет несколько большие привилегии, нежели Chrome. Instant App там не работал до последнего.
Play Core Library позволяет забыть о проблемах доставки. Вы просто подключаете ее как пакет:
implementation 'com.google.android.play:core:1.4.0'
И загружаете нужные вам feature-модули из Google Play.
У библиотеки достаточно функциональный синтаксис, который позволяет скачивать модули поодиночке:
val splitInstallManager = SplitInstallManagerFactory.create(context)
Или сразу пачкой по несколько:
val request =
SplitInstallRequest
.newBuilder()
.addModule("feature1")
.addModule("feature2")
.build()
Вешать listener:
splitInstallManager
.startInstall(request)
.addOnSuccessListener { sessionId -> ... }
.addOnFailureListener { exception -> ... }
Показывать это в UI и реагировать на то, что модуль был установлен.
Play Core Library — хорошее начинание:
- Наконец-то можно перестать надеяться на авось в лице Google Chrome.
- Можно демонстрировать загрузку модулей на UI так, как вам хочется.
- Есть возможность подгружать feature-модули «пачками».
- Появился способ гибкой обработки ошибок, возникающих при загрузке и установке feature-модуля.
- Вам даже не нужно перезагружать приложение после того, как фича будет установлена, потому что есть
SplitCompat.install()
— вызываете ее и можете сразу обращаться к классам из свежеустановленной фичи прямо на лету.
Но я бы вас обманул, если бы сказал, что Play Core Library не заставит вас страдать:
- Код обфусцирован и никак не задокументирован.
- В один
SplitInstallStateUpdatedListener
приходят события со всех активных сессий загрузки — сортировать нужно вручную. Нужно заранее сохранить где-то ID сессии, что в итоге выливается в не очень красивый код. - Корявый, избыточный стейт-менеджмент и обработка ошибок: 9 возможных состояний, 10 возможных ошибок. Сочетания этих ошибок и состояний могут по-разному трактоваться, все состояния и ошибки возвращаются как int.
- Нет возможности нормального тестирования загрузки и установки фичей на локальное устройство — это просто невозможно.
Единственный вариант — задеплоить свою сборку в Google Play в тестовый канал (Internal Testing Channel) и пробовать, что получилось. Если вы находите какой-то баг, то начинаете весь процесс сначала, деплоите обновления и пробуете дальше. Если хотите проверить, как это работает без деплойной сборки, получаете ошибку «-2» и остается только гадать, что же она значит. Это самый часто встречающийся issue в репозитории с официальными samples от Google Dynamic Features.
Нет хорошей практики для навигации между feature-модулями. В официальных примерах, которые показывают, как происходит взаимодействие между feature-модулями, из ситуации предлагают выходить достаточно просто: включите зависимость с одного модуля на другой. В каждом направлении своя зависимость, зачем обратно возвращаться?
Но тогда теряется вся выгода от разбиения на модули. Мы перешли к модулям задолго до того, как возникла необходимость решать такие проблемы. И сделали это отчасти потому, что это упрощает работу с кодовой базой для неопытных разработчиков, которые по неопытности могут с base-activity обратиться к signing-activity — потом ищи это на код-ревью.
Если вы будете связывать все модули зависимостями вдоль и поперек, то хорошо от этого будет только Google, которому не надо будет ничего придумывать насчет навигации.
Мы долго думали, что с этим делать, и в итоге остановились на решении, от которого у меня до сих пор идет дрожь по коже. Оно называется Class.forName
— инстанцирование класса по полному ClassPath
, который ломается любым переименованием класса любого пакета из его пути и т.д.
abstract class ActivityCrossFeatureRoute {
override fun prepareIntent(context: Context): Intent? {
try {
return Intent(context, Class.forName(targetClassPath()))
} catch (e: ClassNotFoundException) {
Logger.e("Activity with the following classpath was not found in the current " +
"application: ${targetClassPath()}. If this activity is the part of Dynamic Feature, " +
"please check if this Dynamic Feature is downloaded and installed on the device" +
"successfully.")
}
return null
}
}
Мне было стыдно кому-либо про это рассказывать до тех пор, пока я не зашел в репозиторий приложения Plaid и не увидел, что они решили проблему с навигацией точно так же и советуют всем делать так на Stack Overflow.
Plaid — это один из самых крутых шоу-кейсов, с ними Google пробует все свои самые последние тенденции, крутые анимации, дизайн-приемы, последние UI-компоненты и, в частности, модульность.
Много, действительно много багов, недоинжиниринга, шероховатостей. Искать баги особо долго не придется.
- Доступ к ресурсам из app-модуля возможен только по полному пути.
Вместо того, чтобы написать так: R.string.primaryColor
, нужно каждый раз в коде писать так: ru.appname.package.R.string.primaryColor
. По-другому билд не соберется, и вот ссылка на официальный issue-трекер.
- Проблема с запуском JobScheduler на O+ девайсах. Официальный Workaround — это ручной запуск
TestJobSchedulerService
:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent serviceIntent = new Intent(this, TestJobSchedulerService.class);
startService(serviceIntent);
}
- Ошибка мёржа манифеста при подключении сторонних библиотек, например, Firebase, Fabric, AndroidX-пакетов и т.д.
Во всех из них используются placeholders application ID в манифесте. Application ID подставляется туда некорректно, из-за чего манифесты не мёржатся, Android Studio не может найти default activity, и вы долго ищете проблему, пока не находите в официальном issue-трекере совет — переопределите все провайдеры и укажите там свой Application ID.
<provider
android:name="com.crashlytics.android.CrashlyticsInitProvider"
android:authorities="ru.app.name.crashlyticsinitprovider"
tools:replace="android:authorities" />
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="ru.app.name.firebaseinitprovider"
tools:replace="android:authorities" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="ru.app.name.fileprovide"
tools:replace="android:authorities" />
- Невозможность множественных зависимостей на сторонние пакеты — один из моих любимых пунктов.
Предположим, два feature-модуля зависят от одной сторонней библиотеки. Но вы не можете поставить прямую зависимость, потому что получите: org.gradle.api.GradleException: [:feature1, :feature2] all package the same library [com.lib.Name:VeryGoodLib]
. Официальный ответ: а вдруг в этих модулях указаны разные версии библиотеки.
Разработчики советуют: добавить в иерархию feature-модуль, в котором нет ничего, кроме зависимости на стороннюю библиотеку, и сделать на него зависимости от двух исходных feature-модулей.
- Крэш вызова InstantApps.isInstantApp(context) в attachBaseContext().
Примеры кода из документации на Android Developers крэшатся, просто потому что они предлагают в attachBaseContext()
обращаться к контексту через this
, а если context==null
, вы даже не можете проверить, запущен ли у вас сейчас InstantApps или нет.
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
if (!InstantApps.isInstantApp(this)) {
SplitCompat.install(this)
}
}
- Баги отображения
ConstraintLayout
при использованииGroups
иBarriers
.
Представьте, вы прошли через всё: собрали сборку, догадались, что ее надо задеплоить в тестовый канал в Google Play, Google Play не вывел красный диалог. Вы дрожащими руками устанавливаете эту сборку на устройство, устанавливаете фичу, переходите на экран — и видите, что вся верстка собралась в кучу в левом верхнем углу. Все потому, что если вы используете Groups
и Barriers
, там есть замечательный метод getPackageName
, который приходит по какой-то причине некорректно. В итоге все ваши вьюшки просто не позиционируются. Все constraints слетают, и всё лежит как попало на экране.
Наконец, после того, как вы и это преодолеете, вы обнаружите, что…
Dynamic Features всё ещё в Beta! Вы не можете задеплоиться в продакшен — вы всё это время были бесплатным тестировщиком для Google!
Но не по мнению Google. Если вы хотите им стать, то можете заполнить Interest Form и, быть может, вам повезет и вам дадут доступ задеплоиться в продакшен.
Есть мнение, что Instant Apps проиграл эту битву. Вряд ли об этом объявят в ближайшие годы, но повышенная информационная активность вокруг AMP негласно это подтверждает.
Мысль, которую я хотел напомнить вам и себе самому — пишите для людей. Трижды подумайте, прежде чем принять какое-то решение, особенно, если занимаетесь разработкой средств, которыми будут пользоваться другие разработчики. Каждое ваше решение и действие скажется на ком-то и, возможно, попортит ему немножко крови. Не хотелось бы стать причиной расстройства для кого-то.
Полезные ссылки
- https://developer.android.com/topic/google-play-instant/overview
- https://developer.android.com/guide/app-bundle
- https://developer.android.com/studio/proiects/dvnamic-delivery
- https://developer.android.com/guide/app-bundle/playcore
- Bundling an App in an Instant (Android Dev Summit '18)
- https://github.com/googlesamples/android-instant-apps
- https://github.com/googlesamples/android-dynamic-features
Мы сделали программу Saint AppsConf, которая уже 21-22 октября в Санкт-Петербурге, еще более насыщенной и разнообразной, чем она была весной. Check it out!
Или подпишитесь на рассылку, telegram, fb — там рассказываем об отдельных докладах и подготовке к конференции.
Автор: Егор