Для чего нужны 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, отличающиеся наличием суффикса у версии для разработчиков).


Решение:


  1. Создаём две конфигурации.
  2. В разработческой указываем суффикс.
  3. Профит!

А теперь разберём подробнее.


Файлы конфигураций


В наших проектах имеется две конфигурации: 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 (ведутся активные работы). Но пока мы имеем то, что имеем. И делаем жизнь проще своими руками.