Всем привет! Меня зовут Никита Спирьянов, я Head of mobile в Friflex. Мы занимаемся разработкой мобильных приложений и высоконагруженных проектов.

Новость о том, что Аpp Store и Google Play могут перестать корректно работать для российских пользователей, спровоцировала рост популярности альтернативных способов дистрибуции приложений, один из них – магазин приложений AppGallery от Huawei.

В этой статье я покажу, как можно разделить GMS (Google Mobile Services) и HMS (Huawei Mobile Services) внутри Flutter-приложения.

Компания Huawei позаботилась о Flutter разработчиках и выпустила большое количество плагинов, которые упрощают публикацию приложения в AppGallery. Кстати, в этой статье я рассказывал о том, как опубликовать мобильное приложение в AppGallery.

Если вы попробуете опубликовать в Google Play приложение, в котором будут зависимости на HMS, то можете получить вот такое письмо:

Причина в следующем:

В политике Google Play есть замечание:

«Any existing app that is currently using an alternative billing system will need to remove it to comply with this update. For those apps, we are offering an extended grace period until September 30, 2021 to make any required changes. New apps submitted after January 20, 2021 will need to be in compliance».

Это значит, что если приложение одновременно поддерживает HMS и GMS сервисы, и в нем есть In-App Purchases, то Google Play не допустит его публикации, а существующим приложениям придется удалить этот функционал.

Получается, что совместное использование HMS и GMS грозит следующими проблемами:

  • увеличенный размер APK приложения;

  • проблемы с совместимостью GMS/HMS для некоторых сервисов;

  • невозможность публикации приложения в Google Play, если в нем есть In-App Purchases.

Поэтому мы решили использовать GMS для iOS/Android (Google Play) и HMS для Android (AppGallery). О том, как разделить HMS/GMS плагины и делать сборки только с GMS или HMS сервисами для приложения, написанного на Flutter, читайте дальше.

Шаг #1. Проанализируем, какие пакеты используют GMS в вашем проекте

Для быстрого поиска GMS-сервисов в приложении можно использовать возможности IDE. Через глобальный поиск по проекту ищем следующее выражение:

com.google.gms:google-services.

Так мы находим все плагины, которые используют GMS. Затем переходим к следующему шагу: поиску альтернативы.

Шаг #2. Найдем HMS-альтернативы

Список уже готовых плагинов для Flutter можно посмотреть тут или тут.

Правда, если вы внимательно посмотрите, то не сможете найти альтернативу crashlytics и некоторым другим сервисам. Все дело в том, что на pub.dev плагины agconnect представлены unverified uploader, но найти их можно в официальной документации от Huawei.

Шаг #3. Рефакторим код (абстрагируемся от GMS-реализаций)

Теперь давайте попробуем абстрагироваться от наших реализаций GMS и добавить HMS-реализацию.

Первая проблема, с которой мы столкнемся, это добавление зависимостей в pubspec. Если мы будем делать реализации внутри приложения, то получим зависимости для HMS и GMS плагинов и мертвый код при отключении одного из них. А так как это противоречит нашей задаче, мы не будем реализовывать это внутри приложения, а вынесем в отдельные пакеты.

Покажу пример с использованием локальных пакетов внутри проекта. Создаем папку local_plugins внутри проекта со следующей структурой:

local_plugins/
../gms
../hms
../interface

В gms и hms у нас будут находится конкретные реализации наших плагинов.
В interface мы будем хранить наши абстракции.
Давайте это реализуем.
Создаем package и называем его analytics_service_interface. В нем будет всего один абстрактный класс, который будет удовлетворять нашим минимальным требованиям в отправке событий для аналитики.

abstract class AnalyticsServiceInterface {
  Future<void> init();

  Future<void> sendEvent(String name, {Map<String, Object?>? parameters});
}

Далее создаем еще два package близнеца в папках gms и hms с одинаковым названием analytics_service (это нужно для того, чтобы сохранялся одинаковый импорт внутри приложения) и делаем реализации нашего AnalyticsServiceInterface.

для gms:


#lib/src/gms_analytics.dart
class GmsAnalyticsService implements AnalyticsServiceInterface {

  @override
  Future<void> init() async {
    await Firebase.initializeApp();
    await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(!kDebugMode);
  }

  @override
  Future<void> sendEvent(String name, {Map<String, Object?>? parameters}) {
    return FirebaseAnalytics.instance.logEvent(
      name: name,
      parameters: parameters,
    );
  }
}
#pubspec.yaml:
... 
 dependencies:
 ...
  analytics_service_interface:
    path: ../../interface/analytics_service_interface
  firebase_analytics: ^9.1.4

для hms:


#lib/src/hms_analytics.dart

class HmsAnalyticsService implements AnalyticsServiceInterface {

  late final HMSAnalytics _service = HMSAnalytics();

  @override
  Future<void> init() async {
    _service.setAnalyticsEnabled(!kDebugMode);
  }

  @override
  Future<void> sendEvent(String event, {Map<String, Object?>? parameters}) {
    return _service.onEvent(event, parameters ?? {});
  }
}
#pubspec.yaml:
...
dependencies:
...
  analytics_service_interface:
    path: ../../interface/analytics_service_interface
  huawei_analytics: ^6.2.0+301

Структура package будет следующая:

Так как мы скрыли реализацию в src, нам необходимо предоставить к ней доступ. Мы это сделаем в файле service.dart. 

Используем export для нашего интерфейса, чтобы он был доступен внутри кода приложения:

export 'package:analytics_service_interface/analytics_service.dart';

и создадим класс, который предоставит нам нужную реализацию AnalyticsServiceInterface.

#service.dart:

import 'package:analytics_service_interface/analytics_service.dart';
import 'src/hms_analytics.dart';

export 'package:analytics_service_interface/analytics_service.dart';

abstract class AnalyticsServiceProvider {
  static AnalyticsServiceInterface getAnalyticsService() =>
      HmsAnalyticsService();
}

Отлично, теперь в pubspec нашего приложения мы добавляем зависимости на эти плагины. Из-за того, что это плагины-«близнецы», мы не сможем использовать одновременно HMS и GMS реализации из-за одинакового названия (мы к этому и стремились). Поэтому пока закомментируем GMS-реализацию.

Один из самых простых и популярных способов внедрения зависимостей — это библиотеки get_it и injectable (подробнее про эти библиотеки можно посмотреть здесь и здесь).

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

dependencies:
  flutter:
    sdk: flutter
  get_it: any
  injectable: any
  app_analytics_service_interface:
    path: platform_plugins/interface/app_analytics_service_interface

#### GMS
#  analytics_service:
#    path: platform_plugins/gms/analytics_service

#### HMS
  analytics_service:
    path: platform_plugins/hms/analytics_service
    

Теперь создаем папку analytics_service внутри приложения и добавляем туда файл di.dart, в котором будет содержаться модуль регистрации для нашего сервиса.

Пример вызова методов сервиса:

 GetIt.instance.get<AppAnalyticServiceInterface>().init();

 GetIt.instance.get<AppAnalyticServiceInterface>().sendEvent('event', parameters: {'key': 'value'});

Как видите, ни в pubspec.yaml, ни в файле модуля нет никаких упоминаний HMS и GMS-реализации.

Поэтому при переключении с

app_analytic_service:
  path: platform_plugins/gms/app_analytics
на
app_analytics_service:
  path: platform_plugins/hms/app_analytics

для нашего приложения ничего не поменяется.

Шаг #4. Настроим флаворы и сборку проекта

Теперь нам нужно настроить Android-часть. 
Сначала настроим флаворы в

app/build.gradle 

android {
    ...

    flavorDimensions "store-type"
    productFlavors {
        google {
            dimension "store-type"
            ...
       
        }
        huawei {
            dimension "store-type"
            ...
        }
    }
}

и добавим папки huawei и google в app/src

в huawei/AndroidManifes.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.friflex.energogarant">
    <uses-permission android:name="com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA"/>
    <application>
        <meta-data
            android:name="com.huawei.hms.client.appid"
            android:value="appid=ID" />

        <meta-data
            android:name="install_channel"
            android:value="AppGallery" />
    </application>


</manifest>

в google/AndroidManifes.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.friflex.energogarant">
    <application>
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="YOUR KEY" />
    </application>
</manifest>

Эти манифесты будут объединены с основным в процессе сборки. Подробнее об этом здесь.
Также нам нужно настроить android/build.gradle

buildscript {
    ext.kotlin_version = '1.6.10'
    repositories {
        ...
        maven { url 'https://developer.huawei.com/repo/' }
    }
   
    def taskName = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
    dependencies {
       ...
        if (taskName.contains("google")){
            classpath 'com.google.gms:google-services:4.3.10'
        }
        if (taskName.contains("huawei")){
            classpath 'com.huawei.agconnect:agcp:1.4.2.301'
        }
    }
}

allprojects {
    repositories {
        ...
        maven { url 'https://developer.huawei.com/repo/' }
    }
}

Переменная taskName будет содержать в себе имя флавора, который был запущен (Huawei или Google). 
Таким образом мы можем управлять зависимостями для разных сборок. 
Сделаем то же самое и в app/build.gradle:

def taskName = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase();
if (taskName.contains("google")){
    apply plugin: 'com.google.gms.google-services'
}
if (taskName.contains("huawei")){
    apply plugin: 'com.huawei.agconnect'
}

Теперь все. Вызвав команду flutter build apk --flavor huawei, запустим задачу assembleHuaweiRelease, которая соберет проект без GMS. Аналогично flutter build apk --flavor google  соберет проект без HMS. 

Остается последний момент с настройкой зависимостей в pubspec. Так как сейчас это находится в ручном режиме, и нам нужно комментировать/раскомментировать код в dependencies для разных типов сборок, это может нас устроить для ручной сборки, но абсолютно не подходит для ci/cd.

К сожалению, Flutter пока не поддерживает объединения pubspec для разных флаворов. 
Есть несколько вариантов решения, я покажу один из них. 
Мы создаем два файла:

pubspec-gms.yaml

pubspec-hms.yaml

И перед запуском — скрипт, который обновляет зависимости из нужного нам файла.
Можно написать свой на dart :), либо использовать любой готовый, например вот этот.

Тогда наш скрипт будет выглядеть следующим образом:

merge_pubspec_gms.sh:

#!/bin/sh
cp pubspec.yaml pubspec-tmp.yaml
yaml-merge pubspec-tmp.yaml pubspec-gms.yaml > pubspec.yaml
rm pubspec-tmp.yaml

merge_pubspec_hms.sh:

#!/bin/sh
cp pubspec.yaml pubspec-tmp.yaml
yaml-merge pubspec-tmp.yaml pubspec-hms.yaml > pubspec.yaml
rm pubspec-tmp.yaml

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


  1. DmitryLyovochkin
    17.05.2022 13:46
    +2

    Хорошая статья, актуальная тема. Спасибо????


  1. Slim_1979
    17.05.2022 17:03
    +2

    Понятно, доходчиво. Надо попробовать. Будет ли статья про ios?