【Flutter 3.7未満】Dart-defineのみを使って開発環境と本番環境を分ける
開発環境 | ステージング環境 | 本番環境 |
---|---|---|
こんにちは、Flutterでのアプリ開発をメインとしている「Altive株式会社」の村松龍之介(@riscait)です!
Flutterにおいて、その環境分けの方法はいくつかありますが、今回は Dart-defines
1つのみを使用して実現する方法で実行してみたのでご紹介します。
少しXcodeやAndroidのbuild.gradleに追記したりが必要ですが、ビルドコマンドがシンプルになることや、
パッケージを使う場合と比べて自動生成ファイルが少なく、取り回しがしやすいことが大きな利点です。
pub.devのパッケージを使用したり、 --flavor
オプションを使用する方法と比べたメリットとデメリットは以下の通りです。
この記事のメリット
- Flutterの環境ごとのビルド設定切り替え方法が分かる
- Flavor分けにパッケージの導入が不要(アイコン生成には使用します)
-
--dart-define
のみでiOS, AndroidネイティブへもFlavor(環境)を伝播できる -
--dart-define=FLAVOR
のみで Bundle ID (Package name) 等を切り替えられる -
main.dart
は1つのままで良い。環境ごとに分ける必要がない - iOSの
scheme
やConfiguration
を作成する必要がない
ちょっと面倒なところ
- Androidでは、
build.gradle
,AndroidManifest.xml
に追記が必要 - iOSでは、
Build Phase
とBuild Pre action
にスクリプトを追加する必要がある - Flutter SDKのバージョンアップグレードで
Dart-defines
のエンコード方法が変わり、対応する必要が出てくる可能性がある
前提
- この記事では以下の3環境に分けていきます
(あくまで一例なので、数や名前は適宜変更してください)- dev (ローカルの開発環境)
- stg (疑似的なDB等を用意し、テストを行うステージング環境)
- prod (ストアで実際に公開するアプリで使用する本番環境)
- Flutter 2.2 以上を想定
- 当記事の手法はFlutter 1.17 以上で利用可能ですが、ネイティブでDart-defineを受け取る際のデコード処理に差異があります。(詳しくは後述)
- 対応OS
- ✅ Android
- ✅ iOS
- ⬜️ Web(ToDo)
当記事で例として使用する環境分け情報
- アプリ名
- dev: Flutter AT.dev
- stg: Flutter AT.stg
- prod: Flutter AT
- アプリID (Bundle ID, Package name)
- dev: jp.co.altive.fat.dev
- stg: jp.co.altive.fat.stg
- prod: jp.co.altive.fat
dev, stg環境の場合はアプリ名とアプリIDにドット繋ぎで環境名を足したいと思います。
もちろん、「アプリ名の方は(dev)のように括弧で表現する」といったことも自由なので、適宜ご調整ください。
必要な下準備
- まだの方は、環境分けを行いたいFlutterアプリを作成、Clone等してください。
- Firebaseを使用している場合は、環境の数分Firebaseプロジェクトを作成し、iOS用の
GoogleService-Info.plist
とgoogle-services.json
をダウンロードしておく。
Iconを環境別に分ける場合に必要な準備
※ Icon画像を環境別に分けない場合は読み飛ばしてください。
環境別のアイコン画像をフォルダに配置します
※ 画像は適宜用意してください
assets/launcher_icon/icon-dev.png
assets/launcher_icon/icon-stg.png
assets/launcher_icon/icon-prod.png
flutter_launcher_iconsパッケージをインストール
flutter pub add flutter_launcher_icons --dev
もしくは pubscpec.yaml に追記して pub get
します。
dev_dependencies:
flutter_launcher_icons: ^0.9.2 # インストール時点での最新版を推奨
Flavorごとの設定ファイルを作成
flutter_launcher_icons-dev.yaml
flutter_launcher_icons-stg.yaml
flutter_launcher_icons-prod.yaml
# flutter_launcher_icons-dev.yaml
flutter_icons:
android: true
ios: true
image_path: "assets/launcher_icon/icon-dev.png"
pubspec.yaml にも設定を追記
# pubspec.yaml
flutter_icons:
android: true
ios: true
アイコン画像の書き出し実行
flutter pub run flutter_launcher_icons:main
✓ Successfully generated launcher icons for flavors
と表示されれば成功です。
iOSでは ios/Runner/Assets.xcassets/AppIcon-{Flavor名}.appiconset/
Androidでは android/app/src/{Flavor名}/mipmap**/launcder.png
に出力されているはずです👍
以上、「Flavor(環境)別アイコンの準備」は終わりです。
ビルド時にコマンドで環境を指定する
アプリ起動(run)やビルド(build)時に環境を分けるために、 --dart-define
というオプションを指定します。
下記の例では、 FLAVOR=dev
(開発環境)という定義を行なっています。
# アプリ起動
flutter run --dart-define=FLAVOR=dev
# アプリビルド
flutter build ios --dart-define=FLAVOR=dev
VS Code や Android Studio を使用している場合は、ボタンやショートカットからアプリ起動することも多いと思います。
その場合は --dart-define=FLAVOR=dev
の設定を追加してください。
VS Code の設定例
VS Code では、 launch.json
で起動コマンドを編集できます。
すでにファイルがある場合は、既存ファイルを編集し、ない場合は .vscode
ディレクトリを作成し、 launch.json
を追加しましょう。
以下のように args
にて --dart-define
を指定可能です。
dev, stg, prod 3環境分用意した例です。
launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run dev",
"request": "launch",
"type": "dart",
"args": [
"--dart-define=FLAVOR=dev"
]
},
{
"name": "Run stg",
"request": "launch",
"type": "dart",
"args": [
"--dart-define=FLAVOR=stg"
]
},
{
"name": "Run prod",
"request": "launch",
"type": "dart",
"args": [
"--dart-define=FLAVOR=prod"
]
}
]
}
お好みで --release
を付与したReleaseビルドバージョンも追加してください。
Android Studio の設定例
Daigoさんが書いてくださいました🙌
Android Studioでの設定方法は↓を確認してください👍
FlutterアプリでFlavorを取得して使いたい場合
まず、Flutterアプリ側でFlavorを取得したい場合の設定を解説します。
例えば、環境ごとにAPI の Base URL や API key を変更したりするのに使うかと思います。
また、起動/ビルドコマンドで指定した dart-define
がきちんと反映されているかも確認することができるので試しておきましょう。
// `--dart-define=FLAVOR=dev` と指定した場合
const flavor = String.fromEnvironment('FLAVOR');
print(flavor) // dev
Android対応
単に dart-define
で環境(Flavor)を指定しただけでは、 Android側に伝わりません。
build.gradle
と AndroidManifest.xml
ファイルを編集する必要があります。
build.gradle を編集して dart-define を受け取る
android/app/build.gradle
を編集します。
ビルド時に指定した dart-define をデコードして受け取ります
// android/app/build.gradle
// dart-define を入れる変数を宣言しています。
// `Key: Value` 形式で初期値を設定することもできます
def dartEnvironmentVariables = [:];
if (project.hasProperty('dart-defines')) {
// カンマ区切りかつBase64でエンコードされている dart-defines をデコードして変数に格納します。
dartEnvironmentVariables = dartEnvironmentVariables + project.property('dart-defines')
.split(',')
.collectEntries { entry ->
def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
[(pair.first()): pair.last()]
}
}
applicationIdSuffix
とアプリ名を追加します
defaultConfigに // android/app/build.gradle
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
+ if (dartEnvironmentVariables.FLAVOR != 'prod') {
+ applicationIdSuffix ".${dartEnvironmentVariables.FLAVOR}"
+ }
+ resValue "string", "app_name", "FlutterAT" +
+ (dartEnvironmentVariables.FLAVOR == 'prod' ? '' : ".${dartEnvironmentVariables.FLAVOR}")
}
- 本番環境以外の
applicationIdSuffix
に.
+FLAVOR
を設定。 - アプリ名として使う変数
string/app_name
の後ろにも.
+FLAVOR
を追加(同じく本番環境以外)
defaultConfigで設定したアプリ名を使用するようにする
android/app/src/main/AndroidManifest.xml
を使用して、 build.gradle
で設定したアプリ名を使用するようにします。
<!-- AndroidManifest.xml -->
- android:label="flutter_app_template"
+ android:label="@string/app_name"
これで環境によってアプリ名が変わるようになりました。
アイコンを切り替えるためのタスクを追加する
flutter_launcher_icons
パッケージを使ってFlavorごとのアイコンを作成しておきます。
src/{flavor_name}/res
ディレクトリに複数の mipmap-xxx
ディレクトリがあり、 ic_launcher.png
が生成されています。
Flavorにより、これらのアイコンを切り替えたいため、 build.gradle
に以下を追記します。
// android/app/build.gradle
task copySources(type: Copy) {
from "src/${dartEnvironmentVariables.FLAVOR}/res"
into 'src/main/res'
}
tasks.whenTaskAdded { task ->
task.dependsOn copySources
}
やっていることは、ビルド時にFlavorディレクトリの res
を src/main/res
にコピーしています。
.gitignore
ファイルに追加
android/app/src/main/res/mipmap*/
は、ビルド時に生成されてFLAVORにより内容が変わるので、Git管理対象外にします。
+ **/android/app/src/main/res/mipmap*/
Firebase対応 (Android)
android/app/src
に各環境(Flavor)と同名のディレクトリ(フォルダ)を作成。
-
android/app/src/dev/
,android/app/src/stg/
,android/app/src/prod/
各Firebaseプロジェクトに作ったAndroidアプリ用の google-services.json
をそれぞれのディレクトリに配置します。
android/app/build.gradle
に追記
環境別ディレクトリに配置した google-services.json
を android/app
にコピーするタスクを定義しています。
// android/app/build.gradle
task selectGoogleServicesJson(type: Copy) {
from "src/${dartEnvironmentVariables.FLAVOR}/google-services.json"
into './'
}
tasks.whenTaskAdded { task ->
if (task.name == 'processDebugGoogleServices' || task.name == 'processReleaseGoogleServices') {
task.dependsOn selectGoogleServicesJson
}
}
.gitignore
ファイルに追加
android/app/google-services.json
は、ビルド時に生成されてFLAVORにより内容が変わるので、Git管理対象外にします。
+ **/android/app/google-services.json
iOS対応
Flavorに対応する xcconfig ファイルを作成
Xcodeにて File -> New -> File (⌘N)
で新規ファイルを追加します。
ファイル形式は Configuration Settings File
を選択します。(config
でフィルタリングするとすぐ出てきます)
ファイル保存場所は ios/Flutter
、ファイル名は dev.xcconfig
のように {Flavor名}.xcconfing
とします。
Flavorの数だけ新規作成しましょう。例: stg.xcconfig
, prod.xcconfig
...
{flavor}.xcconfig
の中に FLAVOR
の値とその他使いたい変数を設定します。
ここでは、 FLAVOR=
の他に、 Bundle IDやアプリ名の末尾等に追加したい APP_ID_SUFFIX
を定義しました。
アプリ名の末尾には別のSuffixを追加したい場合等は、適宜定義を増やして使ってください。
※ DartDefines.xcconfig
は、後ほどシェルスクリプトで生成するファイルなので気にしないでください。
Dart define を受け取る Pre Actionを追加
ビルド時に指定した --dart-define
をiOSで受け取るために、ビルド直前に実行されるスクリプトを追加する必要があります。
もちろん、スクリプトをXcode上から直接書き込んでも良いですが、
-
Runner.xcscheme
に改行がない状態で書き込まれるので差分が見にくい - コメント等で日本語を書くとエンコードされて読めない
というデメリットがあり、新規ファイルを作成して使うことで、好きなエディタ(VS Codeなど)のハイライト機能等を利用しながら編集できる利点もあります。
スクリプトファイル保存
ios/scripts/retrieve_dart_defines.sh
というパスとファイル名で以下の内容の sh
ファイルを保存します。
retrieve_dart_defines.sh
#!/bin/sh
# Dart-defineを書き込んだり、Flavorごとのxcconfigをincludeするファイル
OUTPUT_FILE="${SRCROOT}/Flutter/DartDefines.xcconfig"
# Flutter 2.2 以降で必要な、Dart-Definesのデコード処理
function decode_url() { echo "${*}" | base64 --decode; }
# 最初にファイル内容をいったん空にする
: > $OUTPUT_FILE
IFS=',' read -r -a define_items <<<"$DART_DEFINES"
for index in "${!define_items[@]}"
do
# Flutter 2.2 以降で必要なデコードを実行する
item=$(decode_url "${define_items[$index]}")
# FLAVORが含まれるDart Defineの場合
if [ $(echo $item | grep 'FLAVOR') ] ; then
# FLAVORの値(=の右側)
value=${item#*=}
# FLAVORに対応したXCConfigファイルをincludeさせる
echo "#include \"$value.xcconfig\"" >> $OUTPUT_FILE
fi
done
ここでは…
-
$DART_DEFINES
という変数に格納されているdart-define
を取得して -
FLAVOR
の場合、その値(dev等)を取り出し、DartDefines.xcconfig
に{Flavor名}.xcconfig
をインクルードさせています。
$DART_DEFINES
のすべての変数を書き込んでしまえば楽なのですが、ビルドできなくなってしまったので、 FLAVOR
に絞って書き込んでいます🧐
XcodeのBuild Pre-actions に作成したスクリプトを登録する
- Scheme (Runner) をクリックして
Edit scheme
-> Build を展開してPre-actions
を選択します。 - 「+」ボンタンを押して「New Run Script Action」を選択します。
- 「Provide build settings from」は
Runner
を選択します。 - 先ほど保存したファイルのパスである
${SRCROOT}/scripts/retrieve_dart_defines.sh
を書き込みます。
スクリプトファイルに実行権限を与える
そのままビルドしてもスクリプトファイルが実行されません。
以下のコマンドで実行限限を与えておきましょう。
chmod 755 ios/scripts/retrieve_dart_defines.sh
DartDefines.xcconfig
をインポート
各種xcconfigファイルで 前項のスクリプトで生成される DartDefines.xcconfig
がDebug, Releaseビルド両方で使われるように、
両方の ***.xcconfig
でインクルードします。
ios/Flutter/Debug.xcconfig
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+ #include "DartDefines.xcconfig"
ios/flutter/release.xcconfig
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+ #include "DartDefines.xcconfig"
.gitignore
ファイルに追加
DartDefines.xcconfig
は、ビルド時に生成され、FLAVORにより内容が変わるので、Git管理対象外にします。
+ **/ios/Flutter/DartDefines.xcconfig
アプリ表示名を環境によって変える
ios/Runner/Info.plist
を編集します。
アプリ名に使われる CFBundleDisplayName
にアプリ名 + APP_ID_SUFFIX
を指定します。
<key>CFBundleName</key>
- <string>flutter_app_template</string>
+ <string>FlutterAT$(APP_ID_SUFFIX)</string>
+ <key>CFBundleDisplayName</key>
+ <string>FlutterAT$(APP_ID_SUFFIX)</string>
環境ごとに以下のようなアプリ名が表示されるようになりました👍
- dev: FlutterAT.dev
- stg: FlutterAT.stg
- prod: FlutterAT
のようになります。
Bundle IDを環境によって変える
ios/Runner.xcodeproj/project.pbxproj
を PRODUCT_BUNDLE_IDENTIFIER
で検索するか、
Xcode > Runner > TARGETS Runner > Build Settings の Product Bundle Identifier
を表示して、
$(APP_ID_SUFFIX)
を末尾に追加します。
画像で使っている FLAVOR_SUFFIX
は古い名前です。 APP_ID_SUFFIX
に変更しました
忘れずに Debug, Profile, Release すべてに接尾辞を追加して共通の値になるように注意しましょう。
# ios/runner.xcodeproj/project.pbxproj
- PRODUCT_BUNDLE_IDENTIFIER = "jp.co.altive.fat";
+ PRODUCT_BUNDLE_IDENTIFIER = "jp.co.altive.fat$(APP_ID_SUFFIX)";
これで、環境ごとにアプリのBundle IDが変わるようになりました👏
Appアイコンを環境によって変える
ios/Runner.xcodeproj/project.pbxproj
を ASSETCATALOG_COMPILER_APPICON_NAME
で検索するか、
Xcode > Runner > TARGETS Runner > Build Settings の Primary App Icon Set Name
を表示して、
AppIcon
と指定されている値を AppIcon-$(FLAVOR)
に変更します。
忘れずに Debug, Profile, Release すべて共通の値になるようにしましょう。
# ios/runner.xcodeproj/project.pbxproj
- ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon";
+ ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-$(FLAVOR)";
これで、環境ごとにアプリのアイコンが変わるようになりました👏
Firebase対応 (iOS)
Firebaseを使用していてかつ環境ごとにプロジェクトを分ける場合は、GoogleService-Info.plist
を環境ごとに使い分ける必要があります。
ios
ディレクトリに各環境(Flavor)と同名のディレクトリ(フォルダ)を作成。
-
ios/dev/
,ios/stg/
,ios/prod/
各Firebaseプロジェクトに作ったiOSアプリ用の GoogleService-Info.plist
をそれぞれのディレクトリに配置します。
Select GoogleService-Info.plist
ビルド時に、環境に対応した GoogleService-Info.plist
を使用できるようにするスクリプトを追加します。
-
Build phases
->New run script
を選択して新しいRun Script
を追加 - 名前をわかりやすいように
Select GoogleService-Info.plist
に変更 - スクリプトを記述
-
Output Files
に$(SRCROOT)/GoogleService-Info.plist
を追加 - 既存Scriptである
Copy Bundle Resources
より上に移動
\cp -f ${SRCROOT}/${FLAVOR}/GoogleService-Info.plist ${SRCROOT}/GoogleService-Info.plist
GoogleService-Info.plistへの参照を追加する
(@akaboshinit さん、追加情報ありがとうございます!)
実際には、スクリプトによってコピーされた GoogleService-Info.plist
が使用されることになるのですが、このままでは ${SRCROOT}/GoogleService-Info.plist
ファイルへの参照がなくXcodeで認識できません。
Xcode上でRunnerディレクトリへ GoogleService-Info.plist
ファイル(どの環境のものでも良い)をドラッグ&ドロップで追加してファイルと参照を追加しましょう。
Downloadフォルダ等から、Runnerフォルダ内へドラッグ&ドロップ (Copy items if needed
にチェック)
Runnerフォルダ内に追加されました。
このファイルはビルド時に任意の環境用の GoogleService-Info.plist
に置き換わります🙆♂️
.gitignore
ファイルに追加
ios/GoogleService-Info.plist
は、ビルド時に生成されてFLAVORにより内容が変わるので、Git管理対象外にします。
+ **/ios/GoogleService-Info.plist
Flutterアプリを起動して、Flavorがきちんと伝わっているか確かめる
--dart-define=Flavor
がきちんとネイティブに伝わり、アプリ名やBundle IDがFlavorごとに変更されていることを手軽に確かめるためには、 package_info_plus
パッケージを使用します。
PackageInfo
の下記メソッドを使用して確認できます。
-
.appName
- iOS: アプリ名 (
CFBundleDisplayName
) - Android:
android:label="@string/app_name"
- iOS: アプリ名 (
-
.packageName
- iOS: Bundle ID (
CFBundleIdentifier
) - Android:
applicationId
- iOS: Bundle ID (
宣伝
Altive株式会社では、Flutterアプリの開発・運営を承っております。
お気軽にお問い合わせください🫡
Riverpod の実践入門本を公開中です📘
参考記事
Discussion