Pull to refresh

Flutter Flavoring in a breeze

Reading time8 min
Views6.9K

Хотелось ли вам иметь несколько версий одного приложения?

Чтобы одной командой вы могли собрать приложение под определенное окружение?

Сталкивались ли вы с тем, что одновременно нельзя было установить несколько версий одного приложения на одном устройстве?

Всем привет!

Меня зовут Андрей!

И в этой статье я расскажу, как настроить сборку приложения для разных окружений.

Сразу отмечу, что слова версия, окружение и флейвор (flavor) будут взаимозаменяемыми.

Не смотря на то, что материал называется Flutter Flavoring, бОльшая часть работы будет в нативном пространстве (в папках android/ и ios/). Приведённые мной инструкции используются так же и для нативных приложений, а не только для Flutter приложений.

  • Overview

  • Create the App

  • Переменные окружения в .env

  • Android Flavoring

  • iOS Flavoring

  • App Icons

  • Firebase Projects

  • Заключение

GitHub

Видео версия на 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!

Tags:
Hubs:
+5
Comments1

Articles