Для чего нужны Flavors
Представьте ситуацию: есть приложение с аналитикой. Есть команда разработки, тестировщики и конечные пользователи. И те, и те пользуются одной версией приложения. Допустим мы хотим проанализировать насколько пользователям интересна фича А. Что в этом случае мы делаем? Идём в аналитику и смотрим сколько было использований данной фичи (например, переходов на экран). Но что же мы видим: запредельное число переходов, которое ну никак невозможно с текущей аудиторией, причём все эти переходы были в какой-то определенный отрезок времени. Мы идём дальше и понимаем, что в это время проводились тесты данной фичи. А чуть ранее её разработка. При этом аналитика также отсылалась. Итог: аналитика получается грязной и некачественной.
Здесь можно заменить слово аналитика на любое другое: пуш-нотификации, креш-репортинг и т.д.
И в этой ситуации нас спасает разделение приложения на две версии отличающиеся минимально, например Bundle ID(package-name). Разработчики и тестеры используют только специальную dev версию, а пользователи продовую.
Это как раз и есть одна из задач flavor’ов. Здесь будет использоваться именно flavor, так как именно это название используется Flutter’ом. Люди, которые знакомы с Android-разработкой, думаю сразу узнали этот механизм.

Flavouring Flutter?
Хорошо, мы разобрались с задачей. Но как это реализовать? Всё ли так просто, как пишут?
Давайте сразу определимся: организация flavor’ов — чисто нативная задача. Сама информация о них не будет доступна из dart кода. Поэтому и за способами организации мы пойдём в нативную мобильную разработку.
Android
Здесь всё просто. Ничем не отличается от стандартных способов в android. Конечно может возникнуть вопрос: «А почему бы не использовать buildType?», но об этом позже.
Итак, всё, что нам потребуется в минимальном варианте:
flavorDimensions "release-type" productFlavors { dev { dimension "release-type" applicationIdSuffix ".dev" versionNameSuffix "-dev" } prod { dimension "release-type" } }
И всё, теперь мы можем с лёгкостью запустить команду:
flutter run --flavor dev
на нашем android девайсе.
У некоторых вдумчивых разработчиков может возникнуть вопрос: «А почему не buildType?» Отвечаю: команда Flutter захардкодила buildType под свои нужды. Собственно, вся магия дебаг сборки в этом и заключается.
Немного о типах сборок и различных конфигурациях.
Итак, мы вспомнили про builtTypes. Тут надо немного поговорить о них и об их аналогах в IOS.
Можно провести следующее соответствие:
| Android | IOS |
|---|---|
| build types | build configurations |
| flavors | targets |
И типы сборок и конфигурации — это скорее нечто влияющее на саму сборку, в идеале не влияющее на кодовую базу или различия в приложениях (хотя вопрос спорный). А вот flavor’ы и target’ы — вполне удобный инструмент для создания двух приложений из одного и настройки отличий версии для разработчиков от версии пользователей.
И всё бы было хорошо, и настраивалось именно так, если бы не одно «но»…
Runner — захардкоженный таргет.
Как оказалось, использовать target для реализации flavors на стороне iOS невозможно. Дело в том, что команда разработчиков Flutter по некоторым причинам подвязалась на это имя. И на этом можно было бы закончить всю работу, казалось бы. Но нет. Ведь можно использовать конфигурации сборки.
IOS
Задача: реализовать две конфигурации (dev, prod, отличающиеся наличием суффикса у версии для разработчиков).
Решение:
- Создаём две конфигурации.
- В разработческой указываем суффикс.
- Профит!
А теперь разберём подробнее.
Файлы конфигураций
В наших проектах имеется две конфигурации: dev, prod. Содержимое у них примерно следующее:
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug-dev.xcconfig" #include "Generated.xcconfig" #include "common.xcconfig" bundle_suffix=.dev IDENTIFIER=$(identifier)$(bundle_suffix)
Как мы видим, в них задаётся bundle_suffix.
К слову, кроме этого сам Flutter имеет конфигурации Release и Debug. В них надо также добавить bundle_suffix. Мы же не хотим, чтобы по умолчанию наша версия была версией для пользователей при запуске из своей любимой IDE.
Можно видеть некоторый параметр IDENTIFIER — с ним мы ознакомимся ниже.
Итак, создаём две конфигурации и располагаем в следующих путях:
ios/Flutter/dev.xcconfig ios/Flutter/prod.xcconfig
Создание можно провернуть и через XCode (даже лучше, чтобы они добавились именно как конфигурационные файлы). Делается это с помощью щелчка правой кнопки по Runner → New File → Configuration Settings File → дальше выбираем место сохранения.
Build Configurations. Умножай на два.
Теперь разберёмся с конфигурациями сборки. Открываем Runner.xcworkspace в Xcode и выбираем представление Project.
Там находим кнопку «+» в разделе Configurations и создаём четыре конфигурации: две для Release и две для Debug, где постфиксом пишем название нашего конфига и будущей схемы приложения.
Выглядеть это будет примерно так:

К сожалению, дублировать конфигурации пока необходимо, так как скрипт ответственный за сборку IOS очень чувствителен к неймингу.
Добавление Scheme
Кроме создания файлов конфигурации необходимо также правильно настроить схемы приложения, их также две.

Их создание крайне простое. Важный момент: выберите правильный таргет — Runner.
Теперь выберите пункт Edit Scheme и проставьте необходимые конфигурации на каждом из этапов для схем.
Обновление Info.plist
И последний штрих (спойлер: далеко не последний) — устанавливаем параметр Bundle Identifier в Info.plist как
$(PRODUCT_BUNDLE_IDENTIFIER)$(bundle_suffix)
И вроде бы всё…
И вроде бы всё сделали, проект запускается, на Android всё вообще круто, но если вы вдруг решили использовать fastlane gym для подписи iOS — не получится. И вообще, IOS подпись приложения почему-то не работает… Давайте разберёмся.
No Provisioning Profile
Первая проблема при выгрузке — не найден профайл. Причём в ошибке выведен не тот идентификатор, что мы указали в конфиге.
Как оказалось, установка идентификатора через Info.plist не срабатывает, gym смотрит именно на PRODUCT_BUNDLE_IDENTIFIER, а он у нас одинаковый для всех конфигураций.
Помните загадочный файл common.xcconfig и параметр IDENTIFIER? Вот как раз они и решают эту проблему.
Создаём ещё один конфигурационный файл, в котором мы будем устанавливать по, сути, базовую часть нашего PRODUCT_BUNDLE_IDENTIFIER.
Содержимое описано одной строкой:
identifier=your.bundle.identifier
Подключаем этот файл через include в остальные конфиги и устанавливаем новую User Defined
переменную IDENTIFIER:
#include "common.xcconfig" IDENTIFIER=$(identifier)$(bundle_suffix)
Теперь придётся немного поработать мышкой внутри Xcode. Переходим в наш таргет на вкладку Build Settings:

Далее в поиске ищем Product Bundle Identifier (раздел Packaging):

И меняем значение для всех конфигов на:
$(IDENTIFIER)

Теперь переходим в Info.plist и убираем из строки с идентификатором bundle suffix, оставляя только :
$(PRODUCT_BUNFLE_IDENTIFIER)
Пробуем собрать и подписать. Всё работает как необходимо.
Разные файлы для разных bundle id
Но мы решили подключить аналитику. Если мы используем Firebase, то понадобится два проекта и четыре приложения соответственно (две платформы под две версии).
И что самое главное — нам необходимо иметь два файла google-services.json(Google-Services.Info.plist). На стороне Android всё будет просто: мы создаем папку с названием нашего flavor’а и закидываем туда наш файл.
А вот с IOS нас ждёт крутое приключение с шелл-скриптами и фазами сборки.
Создание и размещения файлов
Первым делом необходимо создать в проекте папку, где будут храниться эти файлы. Мы используем следующую структуру:

Важно: не создаём их через XCode. Они не должны быть привязаны к проекту. Если всё же XCode — это ваша любимая IDE, при создании снимите галочки с пункта Add to target.
Далее располагаем наши файлы в соответствующих папках.
Добавление файлов в приложение во время сборки
Так как файлы не привязаны к проекту, в целевой архив они не попадут. А чтобы они всё-таки туда попали, надо в ручную их туда добавить.
Добавим дополнительный этап сборки в виде Run Script (setup firebase как пример названия):

Обратите внимание на расположение, оно играет решающую роль!
Теперь добавим сам скрипт, как один из вариантов можно использовать подобный:
# Name of the resource we're selectively copying GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist # Get references to dev and prod versions of the GoogleService-Info.plist # NOTE: These should only live on the file system and should NOT be part of the target (since we'll be adding them to the target manually) GOOGLESERVICE_INFO_DEV=${PROJECT_DIR}/${TARGET_NAME}/Firebase/dev/${GOOGLESERVICE_INFO_PLIST} GOOGLESERVICE_INFO_PROD=${PROJECT_DIR}/${TARGET_NAME}/Firebase/prod/${GOOGLESERVICE_INFO_PLIST} # Make sure the dev version of GoogleService-Info.plist exists echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_DEV}" if [ ! -f $GOOGLESERVICE_INFO_DEV ] then echo "No Development GoogleService-Info.plist found. Please ensure it's in the proper directory." exit 1 # 1 fi # Make sure the prod version of GoogleService-Info.plist exists echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_PROD}" if [ ! -f $GOOGLESERVICE_INFO_PROD ] then echo "No Production GoogleService-Info.plist found. Please ensure it's in the proper directory." exit 1 # 1 fi # Get a reference to the destination location for the GoogleService-Info.plist PLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app echo "Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}" # Copy over the prod GoogleService-Info.plist for Release builds if [[ "${CONFIGURATION}" == *-prod ]] then echo "Using ${GOOGLESERVICE_INFO_PROD}" cp "${GOOGLESERVICE_INFO_PROD}" "${PLIST_DESTINATION}" else echo "Using ${GOOGLESERVICE_INFO_DEV}" cp "${GOOGLESERVICE_INFO_DEV}" "${PLIST_DESTINATION}" fi
Мысли после
С помощью этих довольно хитрых манипуляций мы настроили флейворинг. При этом, я очень надеюсь, что решение временное и в скором времени завезут систему сборки под Flutter (ведутся активные работы). Но пока мы имеем то, что имеем. И делаем жизнь проще своими руками.
ссылка на оригинал статьи https://habr.com/ru/company/surfstudio/blog/503864/
Добавить комментарий