Хотелось ли вам иметь несколько версий одного приложения?
Чтобы одной командой вы могли собрать приложение под определенное окружение?
Сталкивались ли вы с тем, что одновременно нельзя было установить несколько версий одного приложения на одном устройстве?
Всем привет!
Меня зовут Андрей!
И в этой статье я расскажу, как настроить сборку приложения для разных окружений.
Сразу отмечу, что слова версия, окружение и флейвор (flavor) будут взаимозаменяемыми.
Не смотря на то, что материал называется Flutter Flavoring, бОльшая часть работы будет в нативном пространстве (в папках android/ и ios/). Приведённые мной инструкции используются так же и для нативных приложений, а не только для Flutter приложений.
-
Overview
-
Create the App
-
Переменные окружения в .env
-
Android Flavoring
-
iOS Flavoring
-
App Icons
-
Firebase Projects
-
Заключение
GitHub: https://github.com/AndrewPiterov/flutter_starter_app/
Видео версия на YouTube:
Overview
Мы настроим сборку приложения для двух окружений: DEVELOPMENT и PRODUCTION.
У каждой версии будут свои
-
иконки
-
наименования
-
application ID
-
переменные окружения, т.к. адрес к API серверу
-
Firebase проекты
Начнём…

Create the App
Для начала создадим наш новый флаттер проект и мигрируем его сразу на null safety
$ flutter create flutter_starter_app $ cd flutter_starter_app && dart migrate --apply-changes
Откроем проект в любимом IDE.
Переменные окружения в .env
Первым делом настроим переменные окружения для нашего проекта.
Эти переменные я предпочитаю хранить в файле assets/.env. И в зависимости какую версию приложения мы собираем, мы указываем в этом файле соответствующие переменные. Изменять этот файл будем в CI/CD (Continuous integration & continuous delivery) в следующих статьях, а пока укажем значения в этом файле один раз и продолжим.
# assets/.env ENVIRONMENT=dev API_URI=https://api.mydev.com
Добавим в pubspec.yaml пакет flutter_dotenv, который облегчит нам считывание этого .env файла:
dependencies: # ... flutter_dotenv: ^4.0.0-nullsafety.0
И укажем, что вместе с проектом идут следующие файлы (assets):
assets: - assets/
Добавляем класс, который будет считывать наши переменные с этого .env файла и предоставлять доступ к этим переменным через свойства:
import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart' as DotEnv; class AppConfig { factory AppConfig() { return _singleton; } AppConfig._(); static final AppConfig _singleton = AppConfig._(); static bool get IS_PRODUCTION => kReleaseMode || ENVIRONMENT.toLowerCase().startsWith('prod'); static String get ENVIRONMENT => env['ENVIRONMENT'] ?? 'dev'; static String get API_URI => env['API_URI']!; Future<void> load() async { await DotEnv.load(fileName: 'assets/.env'); debugPrint('ENVIRONMENT: $ENVIRONMENT'); debugPrint('API ENDPOINT: $API_URI'); } }
Подгрузим наши переменные окружения в самом начале запуска приложения в main.dart:
Future main() async { WidgetsFlutterBinding.ensureInitialized(); await AppConfig().load(); runApp(MyApp()); }
И где-то на скрине в приложении отобразим наши переменные:
Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( AppConfig.ENVIRONMENT, style: TextStyle(fontSize: 50), ), Text( AppConfig.API_URI, style: TextStyle(fontSize: 30), ), ], )
Запускаем приложение:
$ flutter run
Результат:

Изменим значения в .env, перезапустим приложение, и увидим новые значения на экране.
❗️❗️❗️ Не забудьте поместить .env в .gitignore ❗️❗️❗️
На этом настройка в Flutter пространстве (в папке lib/) закончена, следующие настройки будут в нативном пространстве, т.е. в папках android/ и ios/.
Android Flavoring
Для Android настройка очень простая. Достаточно указать следующие параметры в android/app/gradle
android { compileSdkVersion 30 // ... flavorDimensions "starter_app" productFlavors { dev { dimension "starter_app" applicationIdSuffix ".dev" resValue "string", "app_name", "Starter(Dev)" versionNameSuffix ".dev" } prod { dimension "starter_app" resValue "string", "app_name", "Starter" } }
Где указали какие флейворы нам нужны, и у каждого флейвора свой applicationId и наименование.
В AndroidManifest.xml укажем ссылку на переменную app_name с наименованием из флейвора:
<application ... android:label="@string/app_name"
Запускаем приложение на Android под каждую версию:
$ flutter run --flavor=dev $ flutter run --flavor=prod
Результат: установилось два приложения с разными наименованиями.

iOS Flavoring
В iOS нет такого понятия как Flavor, которое есть в Android.И в iOS используется Схемы (Schema) и их Конфигурации (Configuration).
На картинке ниже изображено, что у каждой Схемы есть свои Конфигурации. И у каждой Конфигурации есть свои параметры, которые мы можем кастомизировать. Например, applicationId, название приложения и иконки приложения под разные версии.

Первым делом нам нужно добавить наши Схемы, и добавить к каждой схеме её конфигурации. Для этого мы откроем XCode, и сверху нажимаем на Runner -> New scheme и добавляем нашу новую dev Схему.

Далее добавим devконфигурации. Для этого выбираем Project -> Runner, где видим раздел наших Конфигураций. Чтобы добавить новые конфигурации, нам нужно продублировать имеющиеся конфигурации и назвать их соответсnвующим образом с суффиксом -dev, например:

Дальше переименуем нашу Runner схему вprod

Далее нужно привязать dev Конфигурации к dev схеме. На текущий момент у dev схемы указаны Debug, Release, Profile конфигурации (те, что без суффикса -dev), т.к. мы создали новую dev схему когда еще не было -dev конфигураций.

Переименуем Debug, Release, Profile, добавив к ним суффикс -prod:

Сейчас у нас две схемы с их отдельными конфигурациями. И мы можем кастомизировать параметры для каждой отдельной схемы. И первым делом, выставим каждой конфигурации свой applicationId:

Кастомизируем наименование приложения для каждой отдельной конфигурации:

И добавим в ios/Runner/Info.plist новое свойство для нашей переменной:
<dict> ... <key>CFBundleDisplayName</key> <string>$(APP_DISPLAY_NAME)</string> ... </dict>
Запускаем приложение на iOS под каждую версию:
$ flutter run --flavor=dev $ flutter run --flavor=prod
Результат: установилось два приложения с разными наименованиями.

App Icons
Мы воспользуемся плагином flutter_launcher_icons, который сгенерирует для нас иконки для каждой платформы и для каждой версии по отдельности.
dev_dependencies: # ... flutter_launcher_icons: ^0.8.1
Добавим в корне проекта файлы конфигурации для этого плагина под каждую версию, в которых укажем какие картинки брать для генерации иконок.
# flutter_launcher_icons-dev.yaml flutter_icons: android: true ios: true # image_path: "assets/app_icon/dev.jpg" image_path_android: "assets/app_icon/android_dev.png" image_path_ios: "assets/app_icon/ios_dev.png"
# flutter_launcher_icons-prod.yaml flutter_icons: android: true ios: true # image_path: "assets/app_icon/prod.jpg" image_path_android: "assets/app_icon/android_prod.png" image_path_ios: "assets/app_icon/ios_prod.png"
Запускаем следующую команду генерации иконок:
flutter pub run flutter_launcher_icons:main -f flutter_launcher_icons*
И посмотрим, где добавились сгенерированные иконки:

Для Android все готово, но для iOS нужно снова вернуться в XCode и так же, как и в случае с наименованием и application ID, указать у каждой конфигурации свою иконку:

Запускаем приложение под каждую версию на iOS и Android, и увидим результат — иконки наших уже установленных приложений обновились:


Firebase Projects
Прежде всего создадим два Firebase проекта под каждую версию через firebase console .

В каждом проекте добавим Android и iOS приложения и скачаем файлы конфигурации Firebase проектов:
-
google-services.jsonAndroid приложения — 2 штуки -
GoogleService-Info.plistiOS приложения — 2 штуки

Для теста, можем для каждого Firebase проекта активировать Firestore, в котором одна коллекция secrets с одним элементом, у которого есть поле value. У prod версии значение в value равно PRODUCTION, у dev версии — DEVELOPMENT.

В pubscpec.yaml добавляем Firebase зависимости
dependencies: # ... # Firebase firebase_core: ^1.1.0 cloud_firestore: ^2.0.0
В main.dart проинициализируем Firebase приложение
Future main() async { // ... await Firebase.initializeApp(); runApp(MyApp()); }
И для теста, где-то на скрине приложения отобразим наше значение value
StreamBuilder<QuerySnapshot<Map<String, dynamic>>>( stream: FirebaseFirestore.instance .collection('secrets').snapshots(), builder: (_, snapshot) { if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { return CircularProgressIndicator(); } final first = snapshot.data!.docs.first.data(); return Text( 'Firebase: ' + first['value'], style: TextStyle( fontSize: 25, fontWeight: FontWeight.bold, color: Colors.blue, ), ); }, ),
Настроим iOS и Android для Firebase. Более подробно о настройке можно почитать на официальном сайте.
Настройка Firebase на iOS
В файле ios/Podfile укажем минимальную версию iOS 10
platform :ios, '10'
И в этом же фале в методе target 'Runner' добавим следующую строчку, из-за которой наше приложение будет собираться быстрее:
# ... target 'Runner' do pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '7.11.0' # ... end
Далее кладем файлы конфигурации для Firebase в проекте в папках config/prod и config/dev

И добавим новый Build Phase Script, указанный ниже, который будет во время сборки определенной версии приложения брать соответсвующий файл Firebase конфигурации и помещать его в папку Runner:
environment="default" # Regex to extract the scheme name from the Build Configuration # We have named our Build Configurations as Debug-dev, Debug-prod etc. # Here, dev and prod are the scheme names. This kind of naming is required by Flutter for flavors to work. # We are using the $CONFIGURATION variable available in the XCode build environment to extract # the environment (or flavor) # For eg. # If CONFIGURATION="Debug-prod", then environment will get set to "prod". if [[ $CONFIGURATION =~ -([^-]*)$ ]]; then environment=${BASH_REMATCH[1]} fi echo $environment # Name and path of the resource we're copying GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist GOOGLESERVICE_INFO_FILE=${PROJECT_DIR}/config/${environment}/${GOOGLESERVICE_INFO_PLIST} # Make sure GoogleService-Info.plist exists echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_FILE}" if [ ! -f $GOOGLESERVICE_INFO_FILE ] then echo "No GoogleService-Info.plist found. Please ensure it's in the proper directory." exit 1 fi # Get a reference to the destination location for the GoogleService-Info.plist # This is the default location where Firebase init code expects to find GoogleServices-Info.plist file 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 cp "${GOOGLESERVICE_INFO_FILE}" "${PLIST_DESTINATION}"
Называем эту Build Phase понятным именем и перемещаем ее немного выше:

❗️❗️❗️ Не забудьте поместить GoogleService-Info.plist в .gitignore ❗️❗️❗️
Запускаем приложение и видим результат.
Настройка Firebase на Android
Первое добавим зависимость для плагина google services в android/build.gradle
# android/build.gradle buildscript { dependencies { // ... other dependencies classpath 'com.google.gms:google-services:4.3.3' } }
Используем плагин в android/app/build.gradle
apply plugin: 'com.google.gms.google-services'
Выставим минимальную версию SDK как 21
android { defaultConfig { // ... minSdkVersion 21 // <------ THIS targetSdkVersion 28 multiDexEnabled true } }
Добавим файлы конфигурации Firebase в соответствующие папки каждого флейвора:

❗️❗️❗️ Не забудьте поместить google-services.json в .gitignore ❗️❗️❗️
Запускаем каждую версию на Андроиде и проверяем результат:

Заключение
Таким образом, мы настроили флейворы или сборку разных версий нашего приложения, что у каждой версии свои:
-
application id
-
иконки
-
наименования
-
переменные окружения
-
Firebase бэкенд
Надеюсь материал был полезен для вас.
Всем happy coding!

ссылка на оригинал статьи https://habr.com/ru/post/556382/
Добавить комментарий