Flutter Flavoring

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

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

Всем привет!

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

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

Сразу отмечу, что слова версия, окружение и флейвор (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.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!

Источник: https://habr.com/ru/post/556382/


Интересные статьи

Интересные статьи

Уже 4 и 5 декабря пройдет DartUP — конференция по Dart и Flutter на русском и английском языках. Обычно в это время мы смотрим площадку, печатаем стикеры и запасаем в офисе коробки со...
Всем привет. Меня зовут Дмитрий Андриянов. Два года писал на React Native, сейчас я разработчик в Surf и уже полтора года пишу на Flutter. Когда я только решил серьёзно взяться за Flu...
Периодически мне в разных вариантах задают вопрос, который «в среднем» звучит так: «что лучше: заказать интернет-магазин на бесплатной CMS или купить готовое решение на 1С-Битрикс и сделать магазин на...
Приступая к животрепещущей теме резервного копирования на «Битрикс», прежде всего хотелось бы поблагодарить разработчиков, реализовавших автоматическое резервное копирование в облачное хранилище в вер...
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?