torikatsu923.dev

サイト内検索

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

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

    はじめに

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

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

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

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

    この記事の読み方

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

    この記事の読み方

    環境切り替えの対象

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

    サンプルコード

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

    GitHub - torikatsupg/flutterenv: flutter multi environment app using dart-define

    目次

    基本設定

    この章ではネイティブ側で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を吐き出すため、ワンライナーで以下の処理をしています。

    1. 文字列をカンマ区切りる
    2. カンマで区切った各文字列をbase64でデコードし、カンマで区切る
    3. FLAVOR=xxxが見つかったら=より右側の値を取り出す
    4. 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/に置いておきます。

    • dev.png
    • stg.png
    • prd.png

    画像が用意できたら環境ごとの設定ファイルを用意します。今回は以下の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

    このコマンドによって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を用意します。

    • dev-google-services.json
    • stg-google-services.json
    • prd-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等を利用している場合、この値を使うことで読み込むファイルを切り替えることができます。

    -––