Хотелось ли вам иметь несколько версий одного приложения?
Чтобы одной командой вы могли собрать приложение под определенное окружение?
Сталкивались ли вы с тем, что одновременно нельзя было установить несколько версий одного приложения на одном устройстве?
Всем привет!
Меня зовут Андрей!
И в этой статье я расскажу, как настроить сборку приложения для разных окружений.
Сразу отмечу, что слова версия, окружение и флейвор (flavor) будут взаимозаменяемыми.
Не смотря на то, что материал называется Flutter Flavoring, бОльшая часть работы будет в нативном пространстве (в папках android/
и ios/
). Приведённые мной инструкции используются так же и для нативных приложений, а не только для Flutter приложений.
Overview
Create the App
Переменные окружения в .env
Android Flavoring
iOS Flavoring
App Icons
Firebase Projects
Заключение
Видео версия на YouTube:
Overview
Мы настроим сборку приложения для двух окружений: DEVELOPMENT и PRODUCTION.
У каждой версии будут свои
иконки
наименования
application ID
переменные окружения, т.к. адрес к API серверу
Firebase проекты
Начнём...
![](https://habrastorage.org/getpro/habr/upload_files/06c/7ca/c48/06c7cac4868ebb6cab3e89801ea2ae6d.gif)
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
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/2e8/339/685/2e8339685395d5c2e880b7b02aa28475.jpg)
Изменим значения в .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
Результат: установилось два приложения с разными наименованиями.
![](https://habrastorage.org/getpro/habr/upload_files/509/c97/ebe/509c97ebe3353d4c43a6aacadc3d9d70.jpg)
iOS Flavoring
В iOS нет такого понятия как Flavor, которое есть в Android.И в iOS используется Схемы (Schema) и их Конфигурации (Configuration).
На картинке ниже изображено, что у каждой Схемы есть свои Конфигурации. И у каждой Конфигурации есть свои параметры, которые мы можем кастомизировать. Например, applicationId, название приложения и иконки приложения под разные версии.
![](https://habrastorage.org/getpro/habr/upload_files/950/a05/e17/950a05e17478a92eac5982f61a161195.png)
Первым делом нам нужно добавить наши Схемы, и добавить к каждой схеме её конфигурации. Для этого мы откроем XCode
, и сверху нажимаем на Runner -> New scheme и добавляем нашу новую dev
Схему.
![](https://habrastorage.org/getpro/habr/upload_files/a42/63c/c62/a4263cc62dd718de7644869c9524f90a.gif)
Далее добавим dev
конфигурации. Для этого выбираем Project -> Runner, где видим раздел наших Конфигураций. Чтобы добавить новые конфигурации, нам нужно продублировать имеющиеся конфигурации и назвать их соответсnвующим образом с суффиксом -dev,
например:
![](https://habrastorage.org/getpro/habr/upload_files/057/046/b73/057046b7329e826df9a4d3c70db1167b.gif)
Дальше переименуем нашу Runner
схему вprod
![](https://habrastorage.org/getpro/habr/upload_files/684/d6d/494/684d6d494e8f89f5ef3099303f1804e3.gif)
Далее нужно привязать dev
Конфигурации к dev
схеме. На текущий момент у dev
схемы указаны Debug, Release, Profile
конфигурации (те, что без суффикса -dev
), т.к. мы создали новую dev
схему когда еще не было -dev
конфигураций.
![](https://habrastorage.org/getpro/habr/upload_files/cbe/64c/000/cbe64c000106cdedebc3b9124f941ca0.gif)
Переименуем Debug, Release, Profile,
добавив к ним суффикс -prod:
![](https://habrastorage.org/getpro/habr/upload_files/79a/7d8/aa1/79a7d8aa11e228f9c9ccde09b1ff27d9.gif)
Сейчас у нас две схемы с их отдельными конфигурациями. И мы можем кастомизировать параметры для каждой отдельной схемы. И первым делом, выставим каждой конфигурации свой applicationId:
![](https://habrastorage.org/getpro/habr/upload_files/d22/86e/8b4/d2286e8b406a00edb0205fda9429f91b.gif)
Кастомизируем наименование приложения для каждой отдельной конфигурации:
![](https://habrastorage.org/getpro/habr/upload_files/1b0/ce5/be3/1b0ce5be3d962ed6b36c3d92b6b7c407.gif)
И добавим в ios/Runner/Info.plist
новое свойство для нашей переменной:
<dict>
...
<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>
...
</dict>
Запускаем приложение на iOS под каждую версию:
$ flutter run --flavor=dev
$ flutter run --flavor=prod
Результат: установилось два приложения с разными наименованиями.
![](https://habrastorage.org/getpro/habr/upload_files/90c/7b8/339/90c7b8339471e3e3065524a93eecbc6b.jpg)
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*
И посмотрим, где добавились сгенерированные иконки:
![](https://habrastorage.org/getpro/habr/upload_files/51c/8a5/c4e/51c8a5c4e70289ce6090f5f3025f179d.jpg)
Для Android все готово, но для iOS нужно снова вернуться в XCode
и так же, как и в случае с наименованием и application ID, указать у каждой конфигурации свою иконку:
![](https://habrastorage.org/getpro/habr/upload_files/b5e/c64/6e3/b5ec646e3e60b2c1e682eb3f522df727.gif)
Запускаем приложение под каждую версию на iOS и Android, и увидим результат - иконки наших уже установленных приложений обновились:
![](https://habrastorage.org/getpro/habr/upload_files/fe6/052/b4a/fe6052b4a38805994ce0c3ab23dc5259.jpg)
![](https://habrastorage.org/getpro/habr/upload_files/361/0c8/184/3610c8184f94d31c768cedd7d3ec70c5.gif)
Firebase Projects
Прежде всего создадим два Firebase проекта под каждую версию через firebase console .
![](https://habrastorage.org/getpro/habr/upload_files/06a/db6/240/06adb624054417f3effe383ffff93be9.jpg)
В каждом проекте добавим Android и iOS приложения и скачаем файлы конфигурации Firebase проектов:
google-services.json
Android приложения - 2 штукиGoogleService-Info.plist
iOS приложения - 2 штуки
![](https://habrastorage.org/getpro/habr/upload_files/6f7/94c/867/6f794c867958302378be60543c9c429c.png)
Для теста, можем для каждого Firebase проекта активировать Firestore, в котором одна коллекция secrets
с одним элементом, у которого есть поле value
. У prod
версии значение в value равно PRODUCTION, у dev
версии - DEVELOPMENT.
![](https://habrastorage.org/getpro/habr/upload_files/f5a/8f9/68a/f5a8f968ac017260606a6dbcc4844915.jpg)
В 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
![](https://habrastorage.org/getpro/habr/upload_files/897/0be/403/8970be403404f52754b886644229bd83.gif)
И добавим новый 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
понятным именем и перемещаем ее немного выше:
![](https://habrastorage.org/getpro/habr/upload_files/d31/f13/51a/d31f1351a91e1e9344a13f67638ba4c8.gif)
❗️❗️❗️ Не забудьте поместить 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 в соответствующие папки каждого флейвора:
![](https://habrastorage.org/getpro/habr/upload_files/b45/256/e56/b45256e562c347da9a855d9d15b556fc.jpg)
❗️❗️❗️ Не забудьте поместить google-services.json
в .gitignore
❗️❗️❗️
Запускаем каждую версию на Андроиде и проверяем результат:
![](https://habrastorage.org/getpro/habr/upload_files/4e3/1c2/829/4e31c2829ee189c6f59b30a8919355a8.jpg)
Заключение
Таким образом, мы настроили флейворы или сборку разных версий нашего приложения, что у каждой версии свои:
application id
иконки
наименования
переменные окружения
Firebase бэкенд
Надеюсь материал был полезен для вас.
Всем happy coding!
![](https://habrastorage.org/getpro/habr/upload_files/694/0fe/91f/6940fe91fbb01b74cd91bbd827b7c094.gif)