dart-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける iOS編

【追記】 Android版はこちらです。

[dart-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける Android編](https://qiita.com/tetsufe/items/29cc779592171dcfb7aa)

## dart-defineとは
**dart-defineは、Flutterアプリのビルドや実行の際に、環境変数を設定するためのパラメータです。**`$ flutter build --dart-define FOO=foo` のように使えます。


## Firebase開発環境と本番環境の使い分け
本記事では、dart-defineで環境変数を設定することでFirebaseの開発用と本番用プロジェクトを使い分ける方法をご紹介します。（ただし、今回はiOSビルドの方法のみご紹介します）

また、本記事は[FirebaseのプロジェクトをFlutterに紐づける方法](https://firebase.google.com/docs/flutter/setup?hl=ja)を知っている人を対象としています。


## 他の方法との比較


### 比較対象1. デバッグ・リリースビルドフラグで場合分けする
開発環境と本番環境の２種類の使い分けを行いたいだけならば、デバッグビルドかリリースビルドかの2値で判定することで、dartから環境変数を渡さずとも使い分けることができます。これについてはこちらの記事で解説されています。

[flutter + firebaseで本番環境と開発環境を切り替える](https://qiita.com/ampersand-Dev/items/39f7f6286058435a6bed)

問題は、環境が３つ以上になった場合です。

例えば、以下のように　**3つ以上の環境を使い分けたい場合、デバッグビルドかリリースビルドかの２値で判定するだけでは場合分けの数が足りなくなります。**

- debugビルド かつ 開発用プロジェクト(Firebase)
- debugビルド かつ 本番用プロジェクト(リリース直前など)
- releaseビルド かつ ステージング用プロジェクト(社内リリースなど)
- releaseビルド かつ 本番用プロジェクト

(ステージング: より本番に近い開発/テスト環境)

dart-defineによる環境変数を使う方法やFlavorを使えば、この場合でも対応ができます。

### 比較対象2. Flavor
Flavorは、ビルド時に—-flavor development などとビルドオプションをつけることで、対応したビルド方法を選べるというものです。

iOSであればflavorはbuild scheme、Configuration(ここに環境変数が定義されているイメージ)と対応しており、これらが切り替わることで複数環境の切り替えができるようになっています。

**dart—defineは単に環境変数を設定するだけなので、build scheme、Configurationとの対応関係がなく、これらをあまり知らなくても使うことができる・手順もその分若干少なくなるという点で優れていると考えています。**

一方で、環境が複雑化して環境変数の数が多くなるとflavorを使った方が扱いやすくなるかもしれません。

また、Flavorに関しては、以下の記事が詳しいです。

[Flutterで環境ごとにビルド設定を切り替える — iOS編](https://medium.com/flutter-jp/flavor-b952f2d05b5d)



## 本編

## 1. Firebaseプロジェクトを２つ作成
**本記事の最終目標は、ビルド時に与える環境変数を変えるだけで開発 / 本番用のFirebaseプロジェクトを切り替えられるようにすること** です。

なので、まずは以下の2つの環境に対応する、２つのFirebaseプロジェクトを作ります。

- 開発用プロジェクトの bundle id: com.example.foo.dev
- 本番用プロジェクトの bundle id: com.example.foo

（この時点で、 慎重にいくならば一度FlutterアプリとFirebaseプロジェクトを紐づけて正常に動くかテストしておくと良いと思います。この後もしエラーが出た場合、本記事の手順を間違えているからか通常のFirebaseとの連携手順を間違っているからなのかわからずデバッグが困難になる可能性があります。）


## 2. GoogleService-Info.plistの取得・配置
FirebaseコンソールからGoogleService-Info.plistをダウンロードし、ファイル名を以下のように変更します。

- 開発用: GoogleService-Info-Dev.plist
- 本番用: GoogleService-Info-Prod.plist

これらを**Xcode上で(ファインダーなどを使わないでください)**、以下のように配置します。

- ios/Runner/Firebase/GoogleService-Info-Dev.plist
- ios/Runner/Firebase/GoogleService-Info-Prod.plist

Firebaseディレクトリは、なければ作成してください。

<img width="1055" alt="スクリーンショット 2020-08-03 20.59.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/4aa6fcaf-06bb-eccf-2ade-4439beea1573.png">

**Targetにチェックを入れるのを忘れないようにしてください。**

Xcode上で以下のようになっていればOKです。

<img width="292" alt="スクリーンショット 2020-08-03 20.59.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/7f028079-febe-b957-2542-bcae8f5fca02.png">

次に、Xcode上で ios/Runner/GoogleService-Info.plist という空のファイルを追加します（すでにXcode上で追加したGoogleService-Info.plistが存在する場合は、ここは飛ばしてそのままで構いません）。

**これは、GoogleService-Info.plistをTargetに追加するためで、上の画像のように、 Targetにチェックを入れるのを忘れないようにしてください。そして、実はファイルの実体は存在しなくても良いです。** Targetに追加していない場合はうまくいきません。

Xcode上で以下のようになっていればOKです。

<img width="258" alt="スクリーンショット 2020-08-03 22.04.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/7a33cef7-7bba-7271-16f3-1dab9f858572.png">


### 3. dartの環境変数をXcodeに渡す下準備
dart-defineで設定した環境変数をファイルとして書き出す形でXcodeに渡せるようにします。

まず、デフォルトの環境変数を設定するファイルを作成します。

ios/Runner/Flutter/example-defaults.xcconfig

```
BUNDLE_ID_SUFFIX=
BUILD_ENV=
```

<img width="271" alt="スクリーンショット 2020-08-03 20.59.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/616fda0a-9651-5626-2284-0a7e2963d87d.png">


次に、このxcconfigファイルをXcode側の環境変数として読み込むファイルを作ります。

ios/Runner/Flutter/Debug.xcconfig

```
#include "Generated.xcconfig"
#include "example-defaults.xcconfig"
#include "example.xcconfig"
```

この Generated.xcconfig と example.xcconfig はビルド時に自動生成されるファイルです。ここにdart-defineで渡した環境変数が書き込まれます。

続いて、Release.xcconfigを編集します。


ios/Runner/Flutter/Release.xcconfig

```
#include "Generated.xcconfig"
#include "example-defaults.xcconfig"
#include "example.xcconfig"
```

Release.xcconfig はリリースビルドに対応します。

次に、Xcode上でProduct > Scheme > Edit Schemeを選択します。

<img width="898" alt="スクリーンショット 2020-08-03 21.02.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/5fd822bd-c9ca-ef78-77c6-faeb1da9932f.png">

Build > Pre Action に以下のように入力します。

【修正 2020/8/10】Flutterのバージョンが1.17系の場合はこちら

```sh
echo "$DART_DEFINES" | tr ',' '\n' > ${SRCROOT}/Flutter/example.xcconfig
```

【修正 2020/8/10】Flutterのバージョンが1.20系の場合はこちら

```sh
function urldecode() { : "${*//+/ }"; echo "${_//%/\\x}"; }

IFS=',' read -r -a define_items <<< "$DART_DEFINES"


for index in "${!define_items[@]}"
do
    define_items[$index]=$(urldecode "${define_items[$index]}");
done

printf "%s\n" "${define_items[@]}" > ${SRCROOT}/Flutter/example.xcconfig
```


【修正 2021/9/4】Flutterのバージョンが2.2系以降の場合はこちら

```sh
function urldecode() { echo "${*}" | base64 --decode; }

IFS=',' read -r -a define_items <<< "$DART_DEFINES"


for index in "${!define_items[@]}"
do
    define_items[$index]=$(urldecode "${define_items[$index]}");
done

printf "%s\n" "${define_items[@]}" > ${SRCROOT}/Flutter/example.xcconfig
```


**Provide build settings from に 「Runner」を設定するのを忘れないようにしてください。**

<img width="927" alt="スクリーンショット 2020-08-03 21.03.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/89fb8cd9-385b-1ea1-a66a-507ccb8827b8.png">


このスクリプトによって、ビルド時に以下のように環境変数設定を記述したファイルが自動生成されます。

ios/Runner/Flutter/example.xcconfig

```
BUNDLE_ID_SUFFIX=.dev
BUILD_ENV=dev
```

ここまでで、dart-defineで渡した環境変数をXcode側の環境変数として読み込めるようになりました。


## 4. 環境変数に応じて GoogleService-Info.plist を生成する
Firebase系のモジュールは ios/Runner/GoogleService-Info.plist を読み込むようになっているため、ビルド時に環境変数に応じて開発用か本番用のplistファイルを ios/Runner/GoogleService-Info.plist として上書きするように設定していきます。

ios/replace_google_service_info.sh

```sh
#! /bin/bash

if [[ $BUILD_ENV == *"dev"* ]]; then
  cp $PRODUCT_NAME/Firebase/GoogleService-Info-Dev.plist $PRODUCT_NAME/GoogleService-Info.plist
elif [[ $BUILD_ENV == *"prod"* ]]; then
  cp $PRODUCT_NAME/Firebase/GoogleService-Info-Prod.plist $PRODUCT_NAME/GoogleService-Info.plist
else
  echo "configuration didn't match to Development."
  echo $BUILD_ENV
  exit 1
fi
```

また、パーミッションの問題でこのファイルの実行が許可されずにビルドが失敗する場合もあるので、適宜`$ chmod 755 ios/replace_google_service_info.sh`などとして実行できるように設定してください。

続いて、Xcode上のファイルツリーでルートディレクトリであるRunnerを選択してメニューを開き、「Build Phases」タブを選択します。

ここで「Build Phases」が表示されない場合は、以下の記事を参照してください。
https://azure-nakame.com/?p=689

そして「Build Phases」画面の 左上の方にある + ボタンを押して、「New Run Script Phase」を選択し、Run Script Phaseを作成します。

<img width="905" alt="スクリーンショット 2020-08-03 21.22.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/26b47078-bcd2-476b-daed-b5851c995cae.png">

shellに以下のように先ほど作成した GoogleService-Info.plist を生成するスクリプトを実行するように記入します（↓にスクショあり）。

```
./replace_google_service_info.sh
```

そして、output file の項目にも以下のように生成されたファイルのパスを指定してください。これを行なっていないとエラーになります。

```
$SRCROOT/Runner/GoogleService-Info.plist
```

<img width="903" alt="スクリーンショット 2020-08-03 20.56.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/d3f800d8-3203-899f-cf19-3204eb09f1cc.png">


## 5. 開発用と本番用のBundle Idを分けてFirebaseと対応させる
Product Bundle Indentifier を設定します。これがBundle Indentifierとも同じになるため、これを環境変数によって分けることでFirebaseと対応させることができます。

XcodeでBuild Settings タブを選び、Packaging -> Product Bundle Identifierの共通設定の部分を開き、現在設定されているBundle Identifierの後ろに${BUNDLE_ID_SUFFIX}を追記します。

<img width="1014" alt="スクリーンショット 2020-08-03 21.10.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/6fb0be87-f696-aeb2-94f3-c18b209d92ad.png">

<img width="986" alt="スクリーンショット 2020-08-03 21.10.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/1a90b1b0-c9f2-fbfc-29bb-d799852d1c8e.png">



### ビルド(dart-define環境変数の設定)
ここまでで、長い設定は全て終わりました！一息つく前に、ビルドを実行してしまいましょう。

コマンドでビルドを行う場合は、以下のようにします。

```
$ flutter build ios --debug --dart-define=BUNDLE_ID_SUFFIX=.dev --dart-define=BUILD_ENV=dev
# 本番環境では、BUNDLE_ID_SUFFIX=”” , BUILD_ENV=prod
```

ビルドと実行を同時にする場合は、以下のようにします。

```
$ flutter run -d <deviceID> --debug --dart-define=BUNDLE_ID_SUFFIX=.dev --dart-define=BUILD_ENV=dev
# 本番環境では、BUNDLE_ID_SUFFIX=”” , BUILD_ENV=prod
```

環境変数を入れ替えてみて、無事にFirebaseプロジェクトが使い分けられていればこれで終わりです！

詰まった時は、ここのコメント欄あるいは[僕のTwitter](https://twitter.com/tetsufe_)まで、気軽にご質問ください。


### Android Studioで指定する場合

<img width="617" alt="スクリーンショット 2020-08-03 21.19.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/582c5747-2931-d37e-1b41-4430d48686fc.png">
<img width="1063" alt="スクリーンショット 2020-08-03 21.36.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/80de7c19-4aba-eee4-579c-24018e158788.png">




## おまけ Dartコード上での環境変数の取得
以下のように`String.fromEnvironment()`を使うことで、Dartコード上でも環境変数を取得できます。

良い例

```dart
const foo = String.fromEnvironment('FOO');
```

ダメな例

```dart
Text('${String.fromEnvironment('FOO')}');
```

const指定をしないといけない点に注意です。一度上のダメな例のように書いてしまい、何も表示されず混乱しました。おそらく実行時では環境変数が消えてしまっているためかと思われます。一方constの変数はコンパイル時定数なので環境変数を読み込めるということでしょう。


## 最後に
今回は説明のためにFirebaseプロジェクトは2種類のみとしましたが、3種類以上でも同様の手順で追加していくことができます。

Xcodeの設定は、一度詰まると原因の特定にかなり時間がかかることがあると思います（特に、馴染みがない方にとっては）。なので **詰まった時はここのコメント欄あるいは僕のTwitterまで、気軽にご質問ください。**

また、この記事で目的が達成できたという方は、LGTMボタン👍を押していっていただけると嬉しいです。

https://twitter.com/tetsufe_


## 参考
- [Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything](https://itnext.io/flutter-1-17-no-more-flavors-no-more-ios-schemas-command-argument-that-solves-everything-8b145ed4285d)
  - [サンプルリポジトリ](https://github.com/TatsuUkraine/flutter_define_example)
- [Flutter 1.20 — What you should know before you upgrade your project that uses compile-time variables](https://medium.com/@tatsu.ukraine/what-you-should-know-before-you-upgrade-flutter-in-your-project-with-compile-time-variables-3ec3d2e9ba79)
- [Flutterで環境ごとにビルド設定を切り替える — iOS編](https://medium.com/flutter-jp/flavor-b952f2d05b5d)
- [flutter + firebaseで本番環境と開発環境を切り替える](https://qiita.com/ampersand-Dev/items/39f7f6286058435a6bed)
- [XcodeでDevelop/Staging/Release環境を上手に切り分ける方法](https://qiita.com/Todate/items/a2e6a26731c79bd23e02#targets---build-settings%E3%81%AE%E4%BF%AE%E6%AD%A3)
- [Dart: String.fromEnvironment()の値が取得できない](https://qiita.com/kentt/items/3c2cc5d779ce17b785fd)