はじめに
flutterでビルドのflavor(iOSならscheme)を作る方法をご存知でしょうか??
4月から新規のflutterプロジェクトに携わることになり、その一環でビルド環境(flavor他)を作る方法を知りたいな、、と思い、Flutterの公式ドキュメントを眺めていました。
すると、公式に「Creating flavors for Flutter」という項目があるではないですか!!
上記の項目を開いてみると、いくつか記事が紹介されていますが、リンクが置かれているだけ(若干放置されている感が...)。
ちょっと不親切だな、、と思いつつ、
仕方がないので各記事について、読む+実際に環境を構築してみました!!
このQitaでは、環境構築してみた感想などを簡単にみなさんに共有できればと思います。
対象の記事
以下の5記事が紹介されています。
packageも2つ紹介されていますが、packageに依存するのが嫌なので、今回の調査対象から外しています。
- Creating flavors of a Flutter app
- Flavoring Flutter
- Flutter Ready to Go
- Build flavors in Flutter (Android and iOS) with different Firebase projects per flavor
- Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything
各記事の内容と環境構築してみた感想
記事 | 記事の内容 | 感想 | Github |
---|---|---|---|
Creating flavors of a Flutter app | ・Androidのみ記載されています。 ・flavorの作成方法は、ネイティブのコードを修正します。 ・InheritedWidgetを使ったBuildConfigの管理方法の例を載せてくれています。 ・productFlavor毎にmain.dartを分割して、ビルド時に-tで起動するmain.dartを指定、main.dart内で環境変数を定義します。 |
・productFlavorの定義方法はわかりやすいですが、**他の記事でもproductFlavorの設定方法が解説されているので必読ではない*です。 ・InheritedWidgetを使ったBuildConfigの管理方法を参考になりますが、Flutter Ready to Goの記事のやり方の方が個人的には良いと思います。 |
不明 |
Flavoring Flutter | ・Android(Flavor)とiOS(Scheme)の設定方法が記載されています。 ・ネイティブの設定方法と同じです。 ・Dartの実装についてはあまり書かれていませんでした。 ・productFlavor毎にmain.dartを分割して、ビルド時に-tで起動するmain.dartを指定、main.dart内で環境変数を定義します。 |
・記事の中では画像が表示されていない箇所があるのが読みにくいです。 同じような立ち位置の記事がBuild flavors in Flutter (Android and iOS) with ...なので、そちらを読むのが良いと思います。 |
https://github.com/iakta/flutter_flavors |
Flutter Ready to Go | ・こちらの記事にはFlavor/Schemeの設定方法はあまり書いていません。 ・Dartの実装の方がメインで書かれていました。 ・BuildConfigの管理方法や利用方法がとても参考になります(flavor毎にAPIの接続を変更する実装など)。 ・productFlavor毎にmain.dartを分割して、ビルド時に-tで起動するmain.dartを指定、main.dart内で環境変数を定義します。 |
・flavorの設定方法は別の記事に任せて、本記事はBuildConfigのみ参照するのが良いです。 ・InheritedWidgetよりも記述がシンプルな書き方が紹介されています また、flavor毎にAPIの接続を変更する実装や(テスタ向けに)flavor毎にアプリのバナーの表示を変更する実装例などは参考になります。 |
https://github.com/JHBitencourt/ready_to_go |
Build flavors in Flutter (Android and iOS) with different Firebase projects per flavor | ・Android(Flavor)とiOS(Scheme)の設定方法が記載されています。 ・ネイティブの設定方法と同じです。 ・Dartの実装についてはあまり書かれていませんでした。 ・productFlavor毎にmain.dartを分割して、ビルド時に-tで起動するmain.dartを指定、main.dart内で環境変数を定義します。 |
・記事も全体的にGIFがあり、とても親切だと感じました。 ・iOSのflavorの設定については、正直、2番目の記事xcconfigの手法の方が良いです(xcconfigに修正内容を集約できる)。 ・また、flavor毎にFirebaseのConfig設定を行う場合の手順が書かれているのでFirebase利用者向けでもあります。 |
https://github.com/animeshjain/flavor_test |
Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything | ・Flavor,Schemeを新規に作成することなく、ビルド環境毎の処理を切り替えるための実装方法が書かれています。 ・DartはString.fromEnvironmentからビルド環境変数を取得が可能(main.dartを環境毎に分割せずに済む) ・ネイティブでもビルド環境変数を取得してIconの出しわけなどが可能 |
・個人的にはこちらの設定方法がFlutterエンジニア向けではないかと思います。 ・ネイティブ側の修正量が少ないです。 ・iOSでpreActionの設定が必要なのは面倒。 |
https://github.com/TatsuUkraine/flutter_define_example |
結局どれを採用したの?
5番目の記事「Flutter 1.17...」を採用することとしました。
理由は以下です。
- ネイティブ出身のメンバーが少ないので、ネイティブコードの修正量が少ないものを採用したかった。
- Dart内で簡易にビルド設定を取得することができる。
- 他の記事のやり方で、flutterコマンドで起動時に「-t」で「main-hoge.dart」を指定するのが少し気持ちが悪い(shellスクリプトにすれば良いですが。。。)
- 他の記事のやり方で、 「main-hoge.dart」の似たようなコードがflavorを追加するたびに増えるのが嫌だった
実際の環境構築でやったこと(一部)
今回私の方で、ビルド環境を色々と試したコードはこちらにあります。
記事の中で、flavor/schemeの設定→Dartの実装まで、を抜粋しています。
Creating flavors of a Flutter app (Flutter & Android setup)
(1)flavor設定<Android>
・android/app/build.gradleを編集して、flavorを追加します(ネイティブと同じ)
defaultConfig { // 割愛 }
flavorDimensions "app"
productFlavors {
app1 {
dimension "app"
applicationId "com.example.flavorsexample.app1"
versionCode 1
versionName "1.0"
}
app2 {
dimension "app"
applicationId "com.example.flavorsexample.app2"
versionCode 1
versionName "1.0"
}
}
buildTypes {
・AndroidManifest.xmlを編集します(アプリ名をflavorごとに変える)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.flavorsexample">
[...]
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
[...]
・Stringのリソースファイル(strings.xml)を作成します(アプリ名をflavorごとに変える)
productFlavorごとに以下のようなフォルダとstrings.xmlを作成します。
flavorsexample/android/app/src/app1/res/values/strings.xml
flavorsexample/android/app/src/app2/res/values/strings.xml
flavorsexample/android/app/src/main/res/values/strings.xml
・flutterのコマンドから各flavorのアプリをビルドします。
flutter run --flavor app1
flutter fun --flavor app2
・すると、下記の通りアプリ名の変更、パッケージの変更が確認できます。
App1, App2と表示されました(左上)。
adb shellコマンドで端末内に入り、「pm list package」を実行します。
※端末内のパッケージ一覧を表示してます。
それぞれ、アプリ名が入ったpackage名になっています。
(2)dart実装
ここまではビルドの設定の話でした。ここからFlutter上でビルドの設定に応じて処理を切り替える実装の話です。
・app_config.dart fileを作成します。このファイルでビルド設定を管理します。
import 'package:flutter/material.dart';
class AppConfig extends InheritedWidget {
const AppConfig({required this.appDisplayName,required this.appInternalId, required Widget child}):super(child: child);
final String appDisplayName;
final int appInternalId;
static AppConfig of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppConfig>();
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
・main.dart を main_common.dartにrenameして、中身を少し書き換えます。またmain_app1.dart と main_app2.dartを作成します。
main_common.dart
import 'package:flavorsexample/home_page.dart';
import 'package:flutter/material.dart';
import 'package:flavorsexample/app_config.dart';
void mainCommon() {
// Here would be background init code, if any
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
var config = AppConfig.of(context);
return _buildApp(config?.appDisplayName);
}
Widget _buildApp(String? appName){
return MaterialApp(
title: appName ?? "something wrong",
theme: ThemeData(
primaryColor: const Color(0xFF43a047),
accentColor: const Color(0xFFffcc00),
primaryColorBrightness: Brightness.dark,
),
home: const HomePage(),
);
}
}
main_app1.dart
import 'package:flavorsexample/app_config.dart';
import 'package:flavorsexample/main_common.dart';
import 'package:flutter/material.dart';
void main() {
var configuredApp = const AppConfig(
appDisplayName: "App 1",
appInternalId: 1,
child: MyApp(),
);
mainCommon();
runApp(configuredApp);
}
main_app2.dart
import 'package:flavorsexample/app_config.dart';
import 'package:flavorsexample/main_common.dart';
import 'package:flutter/material.dart';
void main() {
var configuredApp = const AppConfig(
appDisplayName: "App 2",
appInternalId: 2,
child: MyApp(),
);
mainCommon();
runApp(configuredApp);
}
・以下で起動させることで、configがAppConfigに設定されます。
flutter run--flavor app1 -t lib/main_app1.dart
flutter run--flavor app2 -t lib/main_app2.dart
(3)感想
・main_app1.dartなど、main.dartをbuild設定を追加するたびに増やす必要があるのは、管理が手間だと思います。
・InheritedWidgetにflavorの情報を持たせて、ルートのWidgetの方に設定してあげることで、各ScreenのWidgetでもflavorの情報を参照できるつくりはシンプルで良いと思いました。
Flavoring Flutter
(1)flavor設定<Android>
・一つ目の記事同様に、app の方のbuild.gradleを編集して、flavorを追加します。
flavorDimensions "flavor-type"
productFlavors {
development {
dimension "flavor-type"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
}
production {
dimension "flavor-type"
}
}
・下記のコマンドを実行します。
flutter run --flavor development
(2)flavor設定<iOS>
・xcworkspaceを開きます。
・xcconfigを作成します。
・今回はdevelopmentを追加することにします。
・なので、ios/development.xcconfigを作成します(他のものをコピペでOK)。
・bundle_suffixを定義します。
#include "Generated.xcconfig"
bundle_suffix = .dev
・Info.plistファイルを修正します。
このファイルはビルドの設定ファイルのようなものです(xcconfigも同じですが)。
Androidと同じようにBundle IDを変更するために修正しています。
今回はxcconfigに追加している「bundle_suffix」という変数から、Suffixの情報を設定します。
・schemeを追加します。
XCodeの上の方にあるbuildのSchemeを設定するところで、Runnerを開くとプルダウンになるので、New Scheme..を選択します。
・Configurationを追加します。
XCodeの左側にあるタブで「Runner」を選び、PROJECTの方の「Runner」を表示します。
すると、中段くらいで、下記のConfigurationsを設定する箇所があるので、
そこで「+」から、新規のConfigurationを追加します。
新規作成のConfigurationは、名前をRelease-[flavorName] and Debug-[flavorName]といった形にします。
また、Configurationに設定するxcconfigは、手順の1で作成した「development」のxcconfigにします。
・schemeを編集します。
XCodeの上の方にあるbuildのSchemeを設定するところで、Runnerを開くとプルダウンになるので、Edit Scheme..を選択します。
下記の通り、RunとReleaseのConfigurationを設定します。
・run して動かします。
下記はコンソールでの確認ですが、無事に設定できたようです。
(3)dart実装
・mainをflavorのために分割します。
import 'package:flutter/material.dart';
import 'package:flutter_flavors/appEntry.dart'; ・・・起動時のMyApp
import 'package:flutter_flavors/config.dart'; ・・・Enum定義
void main() {
Config.appFlavor = Flavor.DEVELOPMENT;
runApp(new MyApp());
}
上記の中で、appFlavorを設定します。
・下記のコマンドを実行します。
flutter run --flavor development -t lib/main-dev.dart
(4)感想
・最初の記事同様、main_dev.dartなど、main.dartをbuild設定を追加するたびに増やす必要があるのは、管理が手間だと思います。
・iOS側のConfigurationの設定はややこしいですが、本記事に従えば設定は可能と思います。Iconの設定では「Regarding the icons they can be set in the Runner target in the Asset Catalog App Icon Set Name option.」とあります。Runner Targetの設定を開いて、Build Settings で「Asset Catalog Launch Image Set Name」で検索すると設定箇所が出てくるので、そこでflavor毎のIcon名を指定して切り替えることが可能です。
Flutter Ready to Go (flavors, connectivity and more)
(1)dart実装
import 'package:flavorsexample/utils/string_utils.dart';
import 'package:flutter/material.dart';
enum Flavor {
DEV,
QA,
PRODUCTION
}
class FlavorValues {
FlavorValues({required this.baseUrl});
final String baseUrl;
//Add other flavor specific values, e.g database name
}
class FlavorConfig {
final Flavor flavor;
final String name;
final Color color;
final FlavorValues values;
static late FlavorConfig _instance;
factory FlavorConfig({
required Flavor flavor,
Color color = Colors.blue,
required FlavorValues values}) {
_instance = FlavorConfig._internal(
flavor, StringUtils.enumName(flavor.toString()), color, values);
return _instance;
}
FlavorConfig._internal(this.flavor, this.name, this.color, this.values);
static FlavorConfig get instance { return _instance;}
static bool isProduction() => _instance.flavor == Flavor.PRODUCTION;
static bool isDevelopment() => _instance.flavor == Flavor.DEV;
static bool isQA() => _instance.flavor == Flavor.QA;
}
FlavorConfigは、InheritedWidgetで定義することもできますが、この記事ではあえて上記のシングルトンにして、どこでも簡単にアクセスできる作りとしているようです。
InheritedWidgetの場合の欠点は以下の二点がある、と記載があります。
・Blocのウィジェットにアクセスしており、抽象化ルールを破ることになる
・アプリの各レイヤーでパラメータを介してアクセスする必要がある
下記のようなアクセス方法が可能になります。
BannerConfig _getDefaultBanner() {
return BannerConfig(
bannerName: FlavorConfig.instance.name,
bannerColor: FlavorConfig.instance.color
);
}
staticでインスタンスを取得してアクセスする本記事の方がシンプルな記載になっていると感じます。
(例)APIの接続先の変更も以下のようになります。
class BaseApi {
static const int TIMEOUT_SECONDS = 5;
final String baseUrl = FlavorConfig.instance.values.baseUrl;
Future<dynamic> get(String url) async {
final response = await http.get(url)
.timeout(const Duration(seconds: TIMEOUT_SECONDS), onTimeout: _onTimeout);
final responseJson = json.decode(response.body);
return responseJson;
}
Future<http.Response> _onTimeout() {
throw new SocketException("Timeout during request");
}
}
(例)dev環境の場合はAPIを実行しない
class ApiProvider extends BaseApi {
Future<Person> fetchData() async {
var responseJson;
if(FlavorConfig.isDevelopment()) {
responseJson = json.decode(PersonJson.getJson());
await new Future.delayed(new Duration(seconds: 2));
} else {
String url = "${baseUrl}";
responseJson = await get(url);
}
return Person.fromJson(responseJson['person']);
}
}
・それぞれのmainファイルを作成します。
以下は「main_dev.dart」の例ですが、他はGithub上に定義があります。
void main() {
FlavorConfig(flavor: Flavor.PRODUCTION,
values: FlavorValues(
baseUrl: "https://raw.githubusercontent.com/JHBitencourt/ready_to_go/master/lib/json/person_production.json"
)
);
runApp(MyApp());
}
・下記のコマンドで実行することで、flavorの設定を反映します。
flutter run -t lib/main_qa.dart
flutter run -t lib/main_dev.dart
flutter run -t lib/main_production.dart
(2)flavor設定<Android>
・他の記事と同じなので割愛
(3)flavor設定<iOS>
・記事内では紹介がありませんでした。
(4)感想
・BuildConfigのDart内での持ち方は、この記事の方法でも良いなと思っています。InheritedWidgetでも、そこまで手間は変わらないですが。。記述がシンプルになるので。
・flavor毎にAPIの接続を変更する実装は参考になります。
・また、flavor毎にアプリのバナーの表示を変更したり、それをタップすることでビルド設定をダイアログで見える化していたり、テスター向けの設定についても参考になりました。
Build flavors in Flutter (Android and iOS) with different Firebase projects per flavor
(1)flavor設定<Android>
・元の記事がわかりやすいので割愛
(2)flavor設定<iOS>
・元の記事がわかりやすいので割愛
(3)dart実装
・Dartの実装については、Flutter Ready to Go (flavors, connectivity and more)を参考にしてくれ、とのこと。
(4)感想
・記事も全体的にGIFがあり、とても親切です。
・iOSのflavorの設定については、2番目の記事xcconfigの手法の方が良いと思います。xcconfigに設定内容を集約できる方が良いです。
Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything
この記事では、ネイティブでのFlavorやSchemeを追加しない方法が紹介されています。
(1)dart実装
・以下のようなconfigのクラスを作成します。
class EnvironmentConfig {
static const APP_NAME = String.fromEnvironment(
'DEFINEEXAMPLE_APP_NAME',
defaultValue: 'awesomeApp'
);
static const APP_SUFFIX = String.fromEnvironment(
'DEFINEEXAMPLE_APP_SUFFIX'
);
}
「String.fromEnvironment」を実行することで、後述の「flutter runコマンド」での値を取得できます。
・以下のコマンドを実行します。
flutter run --dart-define=DEFINEEXAMPLE_APP_NAME=awesomeApp1 --dart-define=DEFINEEXAMPLE_APP_SUFFIX=.dev
上記で実行すると、Configクラスで「awesomeApp1」と「.dev」が取得できます。
(2)flavor設定<Android>
・app/build.gradleを修正
今回は、productFlavorを使わないので、defaultConfigに書いていきます。
flutterコマンドで定義した環境変数はネイティブからも参照ができるます。以下の方法で取得ができます。
def dartEnvironmentVariables = [
DEFINEEXAMPLE_APP_NAME: 'awesomeApp',
DEFINEEXAMPLE_APP_SUFFIX: null
];
if (project.hasProperty('dart-defines')) {
dartEnvironmentVariables = dartEnvironmentVariables + project.property('dart-defines')
.split(',')
.collectEntries { entry ->
def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
[(pair.first()): pair.last()]
}
}
project.propertyの取得方法はflutterのバージョンで異なるようです、以前はBase64encodeされていなかったようですが、Flutter2.2以降はencodeされるようでした。
上記をapp/build.gradleに定義してあげて、defaultConfig内で読み込みます。
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flavorsexample"
applicationIdSuffix dartEnvironmentVariables.DEFINEEXAMPLE_APP_SUFFIX
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
resValue "string", "app_name", dartEnvironmentVariables.DEFINEEXAMPLE_APP_NAME
}
・ManifestFileを修正(アプリ名を変更するとき)
app/build.gradleでresValueを定義したので、Manifestから読み込みます。
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
・以下のコマンドで立ち上げます。
flutter run --dart-define=DEFINEEXAMPLE_APP_NAME=awesomeApp1 --dart-define=DEFINEEXAMPLE_APP_SUFFIX=.dev
アプリ名や、パッケージ名が書き変わります。
(3)flavor設定<iOS>
・Xcode でios/Runner.xcworkspaceを開きます
・Defaults.xcconfig を作成します。これはDefault用です。
//
// Default.xcconfig
// Runner
//
DEFINEEXAMPLE_APP_NAME=awesomeApp
DEFINEEXAMPLE_APP_SUFFIX=
・Debug、Releaseのconfigファイルから読み込むように、以下修正します。
#include "Generated.xcconfig"
#include "Default.xcconfig"
#include "Defineexample.xcconfig"
・Info.plistを修正します。
アプリ名やBundleIDを変更します。
・PreActionの設定
Androidと同じようにflutterコマンドで定義されている値を取得し、Base64decodeする必要があります。
XCodeの上の方にあるbuildのSchemeを設定するところで、Runnerを開くとプルダウンになるので、Edit Scheme..を選択します。
Runより、Pre-actionsを開いて、「+」から「New Run Script Action」を選択します。
以下の処理をShellで実行させます。
# Type a script or drag a script file from your workspace to insert its path.
function entry_decode() { echo "${*}" | base64 --decode; }
IFS=',' read -r -a define_items <<< "$DART_DEFINES"
for index in "${!define_items[@]}"
do
define_items[$index]=$(entry_decode "${define_items[$index]}");
done
printf "%s\n" "${define_items[@]}"|grep '^DEFINEEXAMPLE_' > ${SRCROOT}/Flutter/Defineexample.xcconfig
・以下のコマンドで立ち上げます。
flutter run --dart-define=DEFINEEXAMPLE_APP_NAME=awesomeApp1 --dart-define=DEFINEEXAMPLE_APP_SUFFIX=.dev
アプリ名や、BundleID名が書き変わります。
(4)感想
・シンプルな実装方法と思いました。コマンドラインからの起動方法しか書いていませんが、AndroidStudioでもこちらを参考に設定が可能です。VSCodeはこちら。
・main.dartをflavor毎に用意する必要がないことが良い点だと思います。ただiOS側でpreActionを設定するのは少々面倒ですね。。
・日本語の記事があるのも良い点かと思います。