Хотелось ли вам иметь несколько версий одного приложения?
Чтобы одной командой вы могли собрать приложение под определенное окружение?
Сталкивались ли вы с тем, что одновременно нельзя было установить несколько версий одного приложения на одном устройстве?
Всем привет!
Меня зовут Андрей!
И в этой статье я расскажу, как настроить сборку приложения для разных окружений.
Сразу отмечу, что слова версия, окружение и флейвор (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/.envENVIRONMENT=devAPI_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.yamlflutter_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.yamlflutter_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.json
Android приложения - 2 штуки -
GoogleService-Info.plist
iOS приложения - 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 =~ -([^-]*)$ ]]; thenenvironment=${BASH_REMATCH[1]}fiecho $environment# Name and path of the resource we're copyingGOOGLESERVICE_INFO_PLIST=GoogleService-Info.plistGOOGLESERVICE_INFO_FILE=${PROJECT_DIR}/config/${environment}/${GOOGLESERVICE_INFO_PLIST}# Make sure GoogleService-Info.plist existsecho "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_FILE}"if [ ! -f $GOOGLESERVICE_INFO_FILE ]thenecho "No GoogleService-Info.plist found. Please ensure it's in the proper directory."exit 1fi# 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 filePLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.appecho "Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}"# Copy over the prod GoogleService-Info.plist for Release buildscp "${GOOGLESERVICE_INFO_FILE}" "${PLIST_DESTINATION}"
Называем эту Build Phase
понятным именем и
перемещаем ее немного выше:
Не забудьте поместить GoogleService-Info.plist
в
.gitignore
Запускаем приложение и видим результат.
Настройка Firebase на Android
Первое добавим зависимость для плагина google
services
в android/build.gradle
# android/build.gradlebuildscript { 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!