torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

【Flutter】dart-definesでアプリの環境を切り替える

はじめに

こんにちは、とりかつ(@torikatsu923)です。

アプリ開発をしていると本番、ステージング、開発と環境を分けたい場合があります。 Flutterでアプリの環境を切り替える方法として、今まではFlavorを使う方法がありました。

最近、DartDefinesという仕組みでも環境を切り替えることができるようになりました。 Flavorと比べ、DartDefinesを使った方が設定がシンプルになります。

今回の記事では目的、OS別(android, iOS)にDartDefinesを利用したアプリの環境切り替え方法を紹介します。

この記事の読み方

この記事では基本設定が終わっている前提で各環境の切り替えの解説をします。そのため、初めに基本設定を読むことを強くお勧めします。 基本設定の後はどの順番で読んでいただいても大丈夫です。

この記事の読み方
この記事の読み方

環境切り替えの対象

  • アプリアイコン
  • アプリのID(bundleId, applicationId)
  • アプリの表示名
  • Firebaseの設定ファイル(GoogleService-Info.plist, google-services.json)
  • Dartのコード中の設定

サンプルコード

サンプルコードは以下のリポジトリです。切り替え対象を全て切り替えられるよう設定した状態になります。

github.com

目次

基本設定

この章ではネイティブ側でDartDefinesの値を利用できるようにする方法を解説します。この基本設定が終わると以下のようにflutter runで環境を指定できるようになります。

$ flutter run --dart-defines=FLAVOR=${環境}

環境

切り替える環境は以下の3つです。

環境 FLAVOR
開発 dev
ステージング stg
本番 prd

iOS

iOSでは環境ごとに.xcconfigを用意しておき、ビルド時にDartDefinesの値から読み込む.xcconfigを変更する方法をとります。

1. 環境に対応するxcconfigを作成する

以下の.xcconfigRunner > 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.xcconfigRelease.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でアクセスできます。
また$SRCROOTiosディレクトリのパスを指しています。

以下のように指定した場合、$DART_DEFINESで得られる値は次のようになります。

--dart-defines=A=valueA,FLAVOR=dev,B=valueB
QT1hLEZMQVZPUj1kZXYsQj1i,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==

どうやら実行時に指定した値以外にFLUTTER_WEB_AUTO_DETECT=truebase64エンコードした値がカンマ区切りで追加されるようです。

この文字列からFLAVORの値を取り出し、include対象を指定したxcconfigを吐き出すため、ワンライナーで以下の処理をしています。

  1. 文字列をカンマ区切りる
  2. カンマで区切った各文字列をbase64でデコードし、カンマで区切る
  3. FLAVOR=xxxが見つかったら=より右側の値を取り出す
  4. flavorに対応するxcconfigincludeする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-dev.yaml
  • flutter_launcher_icons-stg.yaml
  • flutter_launcher_icons-prd.yaml

ファイル名は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

このコマンドによってandroidandroid/app/src/${環境}/resに、iOSios/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 dartEnvironmentVariablesandroid { ...の間である必要があります。

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

まず各環境に対応するxcconfigAPP_ID_PREFIXという変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。

dev.xcconfig

# ...
APP_ID_PREFIX=dev.

stg.xcconfig

# ...
APP_ID_PREFIX=stg.

prd.xcconfig

# ...
APP_ID_PREFIX=

次にxcodeTARGETS > Runner > Build Settingsを開き、Product Bundle IdentifierAPP_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

まず各環境に対応するxcconfigAPP_ID_PREFIXという変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。(アプリIDの手順を先に行っている場合はこの手順は飛ばしてください。)

dev.xcconfig

# ...
APP_ID_PREFIX=dev.

stg.xcconfig

# ...
APP_ID_PREFIX=stg.

prd.xcconfig

# ...
APP_ID_PREFIX=

次にxcodeTARGETS > Runner > Infoを開き、Bundle display nameAPP_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/に置きます。

xcodeTARGETS/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を削除します。削除後にxcodeGoogleService-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等を利用している場合、この値を使うことで読み込むファイルを切り替えることができます。