はじめに
こんにちは、とりかつ(@torikatsu923)です。
アプリ開発をしていると本番、ステージング、開発と環境を分けたい場合があります。 Flutterでアプリの環境を切り替える方法として、今まではFlavorを使う方法がありました。
最近、DartDefinesという仕組みでも環境を切り替えることができるようになりました。 Flavorと比べ、DartDefinesを使った方が設定がシンプルになります。
今回の記事では目的、OS別(android, iOS)にDartDefinesを利用したアプリの環境切り替え方法を紹介します。
この記事の読み方
この記事では基本設定が終わっている前提で各環境の切り替えの解説をします。そのため、初めに基本設定を読むことを強くお勧めします。 基本設定の後はどの順番で読んでいただいても大丈夫です。
環境切り替えの対象
- アプリアイコン
- アプリのID(bundleId, applicationId)
- アプリの表示名
- Firebaseの設定ファイル(GoogleService-Info.plist, google-services.json)
- Dartのコード中の設定
サンプルコード
サンプルコードは以下のリポジトリです。切り替え対象を全て切り替えられるよう設定した状態になります。
目次
- はじめに
- 基本設定
- アプリアイコン
- アプリのID(bundleId, applicationId)
- アプリの表示名
- Firebaseの設定ファイル(GoogleService-Info.plist, google-services.json)
- Dartのコード中の設定
基本設定
この章ではネイティブ側でDartDefinesの値を利用できるようにする方法を解説します。この基本設定が終わると以下のようにflutter run
で環境を指定できるようになります。
$ flutter run --dart-defines=FLAVOR=${環境}
環境
切り替える環境は以下の3つです。
環境 | FLAVOR |
---|---|
開発 | dev |
ステージング | stg |
本番 | prd |
iOS
iOSでは環境ごとに.xcconfig
を用意しておき、ビルド時にDartDefinesの値から読み込む.xcconfig
を変更する方法をとります。
1. 環境に対応するxcconfigを作成する
以下の.xcconfig
をRunner > Flutter
に作成します。
- dev.xcconfig
- stg.xcconfig
- prd.xcconfig
Runner > Flutter
を右クリックし、New File...
を選択します。
NewFile
を選択するとモーダルが表示されるため、Configuration Settings File
を選択します。
選択後Save As
を作成したいファイル名に変更し、create
を押してファイルを作成します。
作成したxcconfig
の中身は以下のようにします。
FLAVOR=devまたはstgまたはprd
dev.xcconfig
の場合はこのようになります。
次に環境ごとのxcconfigをincludeできるようDebug.xcconfig
とRelease.xcconfig
に以下を追記します。
#include "DartDefine.xcconfig"
この後、ビルド時にFlavorに対応するxcconfig
をincludeするDartDefine.xcconfig
というファイルを動的に生成するようにします。そのためここではDartDefine.xcconfig
をincludeするようにしています。
2. DartDefinesに対応するxconfigをincludeするシェルスクリプトを作成する
以下の内容のファイルをios/scripts/dart_define.sh
に置きます。
#/bin/bash echo $DART_DEFINE | tr ',' '\n' | while read line; do echo $line | base64 -d | tr ',' '\n' | xargs -I@ bash -c "echo @ | grep 'FLAVOR' | sed 's/.*=//'" done | ( read flavor echo "#include \"$flavor.xcconfig\"" > $SRCROOT/Flutter/DartDefine.xcconfig )
DartDefinesの値は$DART_DEFINES
でアクセスできます。
また$SRCROOT
はios
ディレクトリのパスを指しています。
以下のように指定した場合、$DART_DEFINES
で得られる値は次のようになります。
--dart-defines=A=valueA,FLAVOR=dev,B=valueB
QT1hLEZMQVZPUj1kZXYsQj1i,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==
どうやら実行時に指定した値以外にFLUTTER_WEB_AUTO_DETECT=true
をbase64エンコードした値がカンマ区切りで追加されるようです。
この文字列からFLAVORの値を取り出し、include対象を指定したxcconfigを吐き出すため、ワンライナーで以下の処理をしています。
- 文字列をカンマ区切りる
- カンマで区切った各文字列をbase64でデコードし、カンマで区切る
FLAVOR=xxx
が見つかったら=
より右側の値を取り出す- flavorに対応する
xcconfig
をinclude
するDartDefine.xcconfig
というファイルを生成する
3. ビルド前に2.のスクリプトが走るようPre-actionを設定する
Runner > edit schema...
を選択しモーダルを表示します。
Build > Pre-action
を開き以下を設定します。
Provide build setting from
Runner
sh "$SRCROOT/scripts/dart_define.sh"
4. .gitignoreの編集
ビルドのたびに差分が発生しないよう.gitignore
に以下を追記します。
.gitignore
DartDefine.xcconfig
iOSでDartDefinesを受け取る基本設定は以上になります。
android
androidではbuild.gradle(app)でDartDefinesから値を取り出して変数に代入する方法をとります。
android/app/build.gradle
に以下を追記します。追記する場所はどこでもいいですが他の環境切り替えの設定でandroid.defaultConfig
でDartDefinesの値を利用することを考えると、android { ...
よりも前にする必要があります。
def dartEnvironmentVariables = project .property('dart-defines') .split(',') .collectEntries { new String(it.decodeBase64(), 'UTF-8') .split(',') .collectEntries { def pair = it.split('=') [(pair.first()): pair.last()] } }
DartDefinesの値はproject
経由でアクセスすることができます。DartDefinesはbase64エンコードされた文字列がカンマ区切りになっているため、split(',')
してさらに各値をsplit('=')
することでDartDefinesに渡された値全てを含むmapを作成しています。
[(pair.first()):pir.last()]
よりmapのキーはDartDefinesに指定した変数名になるため、FLAVORの値は以下のようにアクセスできるようになります。
dartEnvironmentVariables.FLAVOR
androidの基本設定は以上になります。
アプリアイコン
本節ではflutter_launcher_iconsで自動生成したアイコンを使用して環境ごとにアイコンを切り替える方法を紹介します。
アイコンの自動生成
初めにpubspec.yaml
にflutter_laucher_icons`の依存関係を追加します。
... dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.0 flutter_launcher_icons: ^0.9.2 # 依存関係を追記 ...
依存関係を追加したら以下を実行します。
$ flutter pub get
次に環境ごとのアイコン画像を用意します。今回は以下の3つの画像を用意してassets/launcher_icons/
に置いておきます。
画像が用意できたら環境ごとの設定ファイルを用意します。今回は以下の3つを用意しました。
ファイル名はflutter_launcher_icons-${環境名}.yaml
とする必要があり、置く場所はpubspec.yaml
と同じ階層にする必要があります。
それぞれのファイルの中身は以下のようにします。以下はdevの例で、アイコン画像のパス(image_path
)は環境ごとに変えておく必要があります。
flutter_icons: android: true ios: true image_path: "assets/launcher_icons/dev.png"
さらにエラーが出てしまうことを防ぐため、pubspec.yaml
の一番最後に以下を追記します。
flutter: uses-material-design: true # ここから先を追記 flutter_icons: android: true ios: true
最後に以下を実行してアイコン画像を自動生成します。
$ flutter pub run flutter_launcher_icons:main
このコマンドによってandroidはandroid/app/src/${環境}/res
に、iOSはios/Runner/Assets.xcassets/AppIcon-${環境}.appiconset
にアイコンが生成されました。
あとはネイティブ側ビルド時に使用するアイコンを切り替えればOKです。
iOS
iOSでは使用するアイコンを変数によって切り替えることでアイコン切り替えを実現します。
xcodeでTargetのRunner > Build Settings
を開き、Primary App Icon Set Name
を以下に書き換えます。
AppIcon-$(FLAVOR)
これでiOSのアイコンを切り替えることができるようになりました。
android
androidではビルド時に環境に対応するアイコンをandroid/app/src/main/res/
にコピーすることでアイコンを切り替えます。
android/app/build.gradle
を開き以下を追記します。追記する場所はdef dartEnvironmentVariables
とandroid { ...
の間である必要があります。
task copyIcons(type: Copy) { from "src/${dartEnvironmentVariables.FLAVOR}/res" into 'src/main/res' } tasks.whenTaskAdded { it.dependsOn copyIcons }
copyIconsでは標準で用意されているcopy
タスクを使用しています。from
にコピー元、into
にコピー先を指定するだけでファイルをコピーするタスクを簡単に定義することができます。
タスクを定義したらwhenTaskAdded
でタスクの依存関係にcopyIcons
を追加します。これによって何かしらのタスクが実行された際にアイコンをコピーするタスクを実行することができるようになります。
whenTaskAdded
が既に定義されている場合、既存のwhenTaskAdded
のクロージャ内にit.dependsOn copyIcons
を追記してください。
ビルドのたびに差分が発生しないよう.gitignore
に以下を追記します。
.gitignore
**/android/app/src/main/res/mipmap-*/ic_launcher.png
これでandroidでもアイコンを切り替えることができるようになりました。
アプリのID(bundleId, applicationId)
本節では変数を利用してアプリのIDを切り替える方法を紹介します。
iOS
まず各環境に対応するxcconfig
にAPP_ID_PREFIX
という変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。
dev.xcconfig
# ... APP_ID_PREFIX=dev.
stg.xcconfig
# ... APP_ID_PREFIX=stg.
prd.xcconfig
# ... APP_ID_PREFIX=
次にxcodeでTARGETS > Runner > Build Settings
を開き、Product Bundle Identifier
で
APP_ID_PREFIX
を参照するようにします。サンプルのアプリIDはcom.example.flutterenv
のため、以下のようになります。
$(APP_ID_PREFIX)com.example.flutterenv
これでiOSのアプリIDを切り替えられるようになりました。
android
androidにはapplicationIdSuffix
が用意されています。ここに値を設定することでapplicationIdにサフィックスを追加することができます。
android/app/build.gradle
を開いて、defaultConfig
の最後に以下を追記します。
android { // .... defaultConfig { // ... if(dartEnvironmentVariables.FLAVOR == 'dev') { applicationIdSuffix ".dev" } else if(dartEnvironmentVariables.FLAVOR == 'stg') { applicationIdSuffix ".stg" } else if(dartEnvironmentVariables.FLAVOR == 'prd') { applicationIdSuffix "" } else { throw new GradleException("unknown flavor" + dartEnvironmentVariables.FLAVOR + "has passed") } }
これでandroidでアプリIDを切り替えることができるようになりました。
アプリの表示名
本節では変数を参照することでアプリの表示名を切り替える方法を紹介します。それぞれのOSで以下のようにアプリの表示名が切り替わるようにします。
- dev.${アプリ名}
- stg.${アプリ名}
- ${アプリ名}
iOS
まず各環境に対応するxcconfig
にAPP_ID_PREFIX
という変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。(アプリIDの手順を先に行っている場合はこの手順は飛ばしてください。)
dev.xcconfig
# ... APP_ID_PREFIX=dev.
stg.xcconfig
# ... APP_ID_PREFIX=stg.
prd.xcconfig
# ... APP_ID_PREFIX=
次にxcodeでTARGETS > Runner > Info
を開き、Bundle display name
でAPP_ID_PREFIX
を参照するようにします。サンプルのアプリの名前はflutterenv
のため、以下のようになります。
$(APP_ID_PREFIX)flutterenv
これでiOSで環境ごとに表示名を切り替えることができるようになりました。
android
初めにandroid/app/build.gradle
を開いて、defaultConfig
の最後に以下を追記します。
android { // .... defaultConfig { // ... if(dartEnvironmentVariables.FLAVOR == 'dev') { manifestPlaceholders += [appNamePrefix:"dev."] } else if(dartEnvironmentVariables.FLAVOR == 'stg') { manifestPlaceholders += [appNamePrefix:"stg."] } else if(dartEnvironmentVariables.FLAVOR == 'prd') { manifestPlaceholders += [appNamePrefix:""] } else { throw new GradleException("unknown flavor" + dartEnvironmentVariables.FLAVOR + "has passed") } }
manifestPlaceholders
に環境ごとのappNamePrefix
を追加します。こうすることでAndroidManifest.xml
からbuild.gradleで追加した変数を利用することができます。
以下のように代入してしまうとflutterのプラグイン中で追加された変数を上書きしてしまうためビルドに失敗します。
manifestPlaceholders = [appNamePrefix:""]
次にandroid/app/src/main/AndroidManifest.xml
を開き、android:label
でgradleで追加した変数を使用するようにします。
<manifest ... <application android:label="${appNamePrefix}flutterenv" // この行を編集 android:name="${applicationName}" ...
これでandroidのアプリ表示名を切り替えることができるようになりました。
Firebaseの設定ファイル(GoogleService-Info.plist, google-services.json)
本節ではFirebaseの設定ファイルの切り替え方を紹介します。iOS, androidどちらもファイルをコピーする方法をとります。
iOS
以下のファイル名で各環境のGoogleService-Info.plistを用意します。
- devGoogleService-Info.plist
- stgGoogleService-Info.plist
- prdGoogleService-Info.plist
ファイルが用意できたらios/Firebase/
に置きます。
xcodeでTARGETS/Runner/Build Phases
を開き、右上の+
ボタンからNew Run Script Phase
を選択します。
追加できたら以下のスクリプトを設定します。
cp -f ${SRCROOT}/Firebase/${FLAVOR}GoogleService-Info.plist ${SRCROOT}/GoogleService-Info.plist
Phaseの名前はわかりやすいようにCopy GoogleServiceinfo.plist
に変更しています。
最後にGoogleService-Info.plist
の参照をプロジェクトに追加します。
まずios
直下にGoogleService-Info.plist
を置きます。このファイルは後で削除するためファイル名さえあっていれば中身はなんでもOKです。
次にfinderからxcodeのRunner直下にGoogleService-Info.plist
をドラッグ&ドロップします。
ダイアログが表示されたらFinishを押します。これでファイルの参照を作ることができました。
finderからGoogleService-Info.plist
を削除します。削除後にxcodeでGoogleService-Info.plist
が赤くなっていれば成功です。
ビルドのたびに差分が発生するのを避けるために.gitignore
に以下を追記します。
.gitignore
**/ios/GoogleService-Info.plist
以上でビルド時にiOSのFirebaseの設定ファイル切り替えられるようになりました。
android
以下のファイル名で各環境のgoogle-services.json
を用意します。
ファイルが用意できたらandroid/app/src/firebase
に置きます。
次にandroid/app/build.gradle
を開き以下のタスクを定義します。
build.gradle
task copyFirebaseSource(type: Copy) { from "src/firebase/${dartEnvironmentVariables.FLAVOR}-google-services.json" into './' rename { String fileName -> fileName = "google-services.json" } } tasks.whenTaskAdded { // ... it.dependsOn copyFirebaseSource }
ここでもcopyタスクを利用します。コピーする際にファイル名を変更する必要があるため、renameにファイル名変更の処理を指定しています。
すでにwhenTaskAdded
が定義されている場合、既存のwhenTaskAdded
の最後にit.dependsOn copyFirebaseSource
を追記します。
ビルドのたびに差分が発生するのを避けるために.gitignore
に以下を追記します。
.gitignore
**/android/app/google-services.json
以上でビルド時にandroidのFirebaseの設定ファイル切り替えられるようになりました。
Dartのコード中の設定
DartコードからFlavorの値を利用したい場合があります。その際は以下のようにしてアクセスすることができます。
String.fromEnvironment('FLAVOR')
dotenv等を利用している場合、この値を使うことで読み込むファイルを切り替えることができます。