В марте Google Play стал рассылать письмо-предупреждение для разработчиков, использующих Huawei Mobile Services в своих мобильных приложениях. И в этом письме было сказано, что использование HMS в сборках для Google Play противоречит политикам стора приложений, а на решение проблемы дается 120 дней. В противном случае Google Play обещает перестать принимать обновления для таких приложений.

После получения такого “письма счастья” мы окончательно убедились, что наша единая сборка приложения для всех сторов с переключением платформенных сервисов в рантайме – не самое надежное решение в столь изменчивом мире. В общем, мы решили оперативно перейти на раздельные сборки. Особенность нашего решения в том, что мы сохранили GMS+HMS сборку приложения для AppGallery, добавив в наш проект возможность сделать чистую GMS-сборку для Google Play. Мы использовали флейворы, но в связке с многомодульностью нам удалось затащить под флейворы лишь минимальное количество кода. 

В этой небольшой статье я расскажу о нашей архитектуре разделения сборок для разных сторов, проблемах, с которыми столкнулись, а еще поделюсь статистикой по активным HMS-пользователям. 

Ищем новое решение

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

Структура модулей для интергации с платформенными сервисами
Структура модулей для интергации с платформенными сервисами
  • platform-services:common – модуль, в котором описаны общие интерфейсы для работы с платформенными сервисами без привязки к конкретному вендору. Именно он подключается по месту использования платформенных сервисов в фичах приложения.

  • platform-services:gms/hms/fallback – модули, которые реализуют интерфейсы из модуля common. fallback – заглушечная реализация на случай, когда на девайсе нет ни GMS, ни HMS. Далее я буду называть эти модули вендорными. Это единственное место, где мы подключаем внешние HMS/GMS-зависимости от Huawei/Google.

  • platform-services:integration – этот модуль отвечает за связывание интерфейсов из common с их реализациями из вендорных модулей. One ring to rule them all… Единственное, что он предоставляет наружу – это DI-провайдеры common-интерфейсов для подключения в граф зависимостей на уровне app-модуля.

В варианте с единой сборкой DI-провайдеры из integration-модуля в рантайме определяли наличие сервисов на девайсе и возвращали необходимую реализацию common-интерфейсов. Мы хотели сохранить тот же принцип работы, только перевести его в компайлтайм. 

Какие варианты мы рассматривали:

  • Отдельные application-модули для AppGallery и Google Play. В нашем случае это требовало серьезных затрат на вынос общей application-части, а при реализации “в лоб” мы столкнулись бы с большим количеством копипасты. 

  • Кастомный Gradle-параметр, который можно передать при вызове сборки приложения. На этапе Gradle-конфигурации в зависимости от значения параметра подключаем модули с реализациями для GMS/HMS. Они должны иметь полностью идентичный API – названия классов, методов, пакетов, чтобы использующий их код скомпилировался. С таким способом мы бы лишились встроенной в IDE поддержки переключения Build Variants: содержимое каких-то модулей никогда не распознавалось бы студией. Также этот способ делает сборку менее удобной и понятной: нужно не забывать про существование кастомного параметра и про необходимость его пробрасывать. 

  • Product flavors. Фича Android Gradle Plugin для кастомизации способов сборки приложения через source set-ы. Создаем отдельные директории (source set-ы) для флейворов, исходники и ресурсы которых будут оверрайдить одноименные исходники и ресурсы в main source set. Из всех возможных комбинаций флейворов и buildType-ов AGP сгенерит нам множество assemble-тасков – вариантов сборки. Большое количество вариантов сборки и кастомизаций для отдельных вариантов может привести к source set hell. У нас в проекте уже есть флейворы в разрезе стран, через которые мы подменяем небольшое количество ресурсов: hhru и rb. А также 3 билдтайпа: debug, prerelease и release. Итого 6 вариантов сборки. С добавлением флейворов для платформенных сервисов нам не хотелось удваивать их количество и заводить для каждого варианта по своему source-сету. 

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

Разделяем сборки через флейворы

Мы добавили в проект новый dimension флейворов со значениями gms и hms для поставки в Google Play и AppGallery соответственно. В hms-флейворе мы хотели бы подключать и HMS-, и GMS-зависимости, чтобы получать оптимальный пользовательский опыт, когда приложение из AppGallery установлено на девайсе с поддержкой GMS. hms-флейвор – это та же самая единая сборка с рантайм-переключением, которую мы делали раньше, а gms-флейвор добавляет возможность собрать сборку без HMS для Google Play.

Схема подключения модулей друг к другу. Направление стрелок – связь “зависит от”. Обратите внимание на использование hmsImplementation для hms-модуля.
Схема подключения модулей друг к другу. Направление стрелок – связь “зависит от”. Обратите внимание на использование hmsImplementation для hms-модуля.

Флейворы для платформенных сервисов у нас содержатся только в двух местах: integration и application модулях, а специфичные для этих флейворов source set-ы есть только у модуля integration. Мы хотели сделать так, чтобы под source set-ами флейворов у нас было как можно меньше кода. Потому что в Android Studio для полноценной работы с кодом в source set-ах флейворов необходимо переключать активный build variant на одноименной вкладке и дожидаться синхронизации проекта. Это мешает, например, проводить поиск и авторефакторинг встроенными в Android Studio инструментами.

При поиске решений для разделения билдов часто попадаются материалы, где авторы предлагают полностью выносить во флейворы реализации общих интерфейсов. По такому принципу, например, работает официальный конвертер от Huawei. Но благодаря разделению реализаций общих интерфейсов на разные модули, а не source set-ы, в нашем решении под флейворами остается существенно меньше кода – только DI-провайдеры, которые обычно пишутся в несколько строк.

Содержимое кода в платформенных флейворах
Содержимое кода в платформенных флейворах

Приведу в качестве примера провайдер для common-интерфейса, отвечающего за работу с пушами.

Реализация из gms-флейвора:

// platform-services/integration/src/gms/…/PushTokenSourceProvider.kt
// (модуль integration)

class PushTokenSourceProvider(
   private val firebasePushTokenSource: FirebasePushTokenSource,
   private val fallbackPushTokenSource: FallbackPushTokenSource,
   private val platformServices: PlatformServices,
) : Provider<PushTokenSource> {

   override fun get(): PushTokenSource {
       return if (platformServices.isAvailable(Type.GOOGLE)) {
           firebasePushTokenSource
       } else {
           fallbackPushTokenSource
       }
   }

}

Реализация из hms-флейвора (работающего как с HMS, так и с GMS):

// platform-services/integration/src/hms/…/PushTokenSourceProvider.kt
// (модуль integration)

class PushTokenSourceProvider(
   private val firebasePushTokenSource: FirebasePushTokenSource,
   private val huaweiPushTokenSource: HuaweiPushTokenSource,
   private val fallbackPushTokenSource: FallbackPushTokenSource,
   private val platformServices: PlatformServices,
) : Provider<PushTokenSource> {

   override fun get(): PushTokenSource {
       return when (platformServices.getPreferredPlatformServices()) {
           Type.GOOGLE -> firebasePushTokenSource
           Type.HUAWEI -> huaweiPushTokenSource
           null -> fallbackPushTokenSource
       }
   }

}

В этом примере PushTokenSource – вендоронезависимый интерфейсом из модуля common, а Firebase/Huawei/FallbackPushTokenSource – его реализации из вендорных модулей. 

Проблемы

Конечно, не обошлось и без проблем. Например, для опционального подключения Gradle-плагина agconnect от Huawei мы не нашли более эффективного решения, чем явно проверять название запускаемой таски: 

if (isRunForHMSBuild()) {
   plugins {
       id("com.huawei.agconnect")
   }

   configure<AGCPExtension> {
       manifest = false
       enableAPMS = false
   }
}

fun isRunForHMSBuild(): Boolean {
   val requestedTasks = gradle.startParameter.taskRequests.toString()
   val regex = "assemble(.+)Hms(.+)".toRegex()
   return regex.containsMatchIn(requestedTasks)
}

Еще мы столкнулись с проблемами из-за наличия в source set-ах нашего приложения директорий вроде hhruRelease и rbRelease. hhru и rb – это наши флейворы по странам, и в этих source set-ах мы храним ресурсы, которые специфичны одновременно как для страны, так и для релизной сборки. Но мы не хотим, чтобы эти ресурсы отличались между gms и hms флейворами. Android Gradle Plugin обязывает нас продублировать такие source set-ы с уточнением флейвора платформенных сервисов. То есть создать новые директории hhruGmsRelease и hhruHmsRelease для hhruRelease и дублировать ресурсы между ними. Мы не очень хотели удваивать source set-ы, чтобы в дальнейшем не менять одно и то же в двух местах сразу. Поэтому на уровне application-модуля мы изменили корневые директории source set-ов. Таким образом и hhruGmsRelease, и hhruHmsRelease стали подхватываться из общей директории hhruRelease.

Статистика

Полноценно внедряя HMS год назад, мы решили добавить аналитику наличия платформенных сервисов на девайсах пользователей. За апрель 2022 статистика по ней выглядит вот так:  

Наличие платформенных сервисов

Процент запусков приложения

Только GMS

79.45%

Только HMS

2.84%

GMS и HMS

17.69%

Девайсы без платформенных сервисов

0.02%

Значит ли это, что из AppGallery наше приложение ставит едва ли 3% пользователей? 

На самом деле количество новых установок из AppGallery за апрель составляет 10% от  общего количества инсталлов приложения. По большей части это пользователи на Huawei и Honor девайсах, у которых установлены как GMS, так и HMS. То есть решение поддержать в сборке для AppGallery оба типа сервисов оказалось оправданным.

Подводим итоги

Итак, благодаря разбиению на модули, у нас получилось вынести под флейворы совсем небольшое количество кода и сохранить рантаймовый GMS+HMS вариант приложения для AppGallery. Также потребовалось провести небольшие изменения на уровне инфраструктуры CI/CD: добавить дополнительные сборки на PR-ах и релизах, настроить автоматический паблишинг релизов в AppGallery через Publishing API от Huawei. 

На этом у меня всё. Если вы тоже внедряли у себя в проекте поддержку HMS, пишите в комментариях, каким путем решили пойти вы, с какими проблемами сталкивались и делитесь советами.

Комментарии (4)


  1. Wolframium13
    12.05.2022 10:15
    +2

    Ох эти "политики стора приложений".


    1. r2d
      12.05.2022 14:54
      +2

      И не говорите! Скоро сборка для AppGallery, сборка для Google Play, сборка для NashStore


  1. Jkstop
    12.05.2022 23:17
    +1

    Но зачем?

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


    1. horseunnamed Автор
      13.05.2022 11:56
      +1

      К сожалению, это не единственный случай. К нашей прошлой статье про единую сборку оставлял комментарий разработчик из Huawei с описанием рисков. Сначала мы надеялись с таким не столкнуться, но, столкнувшись, решили устранить риски наверняка :)

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