Хотелось ли вам иметь несколько версий одного приложения?
Чтобы одной командой вы могли собрать приложение под определенное окружение?
Сталкивались ли вы с тем, что одновременно нельзя было установить несколько версий одного приложения на одном устройстве?
Всем привет!
Меня зовут Андрей!
И в этой статье я расскажу, как настроить сборку приложения для разных окружений.
Сразу отмечу, что слова версия, окружение и флейвор (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 проекты
Начнём...
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.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 =~ -([^-]*)$ ]]; 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!