Для чего нужны Flavors
Представьте ситуацию: есть приложение с аналитикой. Есть команда разработки, тестировщики и конечные пользователи. И те, и те пользуются одной версией приложения. Допустим мы хотим проанализировать насколько пользователям интересна фича А. Что в этом случае мы делаем? Идём в аналитику и смотрим сколько было использований данной фичи (например, переходов на экран).
Но что же мы видим: запредельное число переходов, которое ну никак невозможно с текущей аудиторией, причём все эти переходы были в какой-то определенный отрезок времени. Мы идём дальше и понимаем, что в это время проводились тесты данной фичи. А чуть ранее её разработка. При этом аналитика также отсылалась. Итог: аналитика получается грязной и некачественной.
Здесь можно заменить слово аналитика на любое другое: пуш-нотификации, креш-репортинг и т.д.
И в этой ситуации нас спасает разделение приложения на две версии отличающиеся минимально, например Bundle ID(package-name). Разработчики и тестеры используют только специальную dev версию, а пользователи продовую.
Больше кейсов команды Surf >>
Это как раз и есть одна из задач 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 (ведутся активные работы). Но пока мы имеем то, что имеем. И делаем жизнь проще своими руками.
Больше полезного про Flutter — в телеграм-канале Surf Flutter Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!