【Flutter】--dart-define でFirebaseの環境を分ける

##はじめに
Firebaseをバックエンドとして、FlutterでAndroid, iOSアプリを開発していると`flutter run` する時に環境を分けたい。（Firebaseプロジェクトの接続先を変えたい）
すでにある他の記事を参考に達成できたので、記憶が新しいうちに自分用にまとめておく。
--flavorを使った分け方もありますが、今回は `--dart-define=XXX=YYY`とビルドモードだけを使って環境を分けます。（とりあえずiOSのみ。また後日Androidもまとめたい）

####前提

- Flutterのバージョン: 2.2.3
- FlutterとFirebaseの繋ぎ方を知っている（Firestoreからデータを取得したことがある）

##本編
まずは、Flutterアプリを作成
（今回は`base_ogen3`というFlutterアプリを作成しました。）

```
$ flutter create base_ogen3
```


###Firebaseにプロジェクトを用意
まずは、Firebaseで分けたい環境の数だけプロジェクトを作成する。
今回は３つ作ります
（プロジェクト名はなんでもよい。ただし、語尾を「-dev」のようにすると環境がわかりやすい）

- base-ogen3-dev
- base-ogen3-stg
- base-ogen3-prod

![firebase projects.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/6b50421f-bd8d-da90-922d-eb25e3752392.png)

次にそれぞれにiOSアプリを追加します。
追加時、Bundle Id はそれぞれ別アプリ化したいので以下を用意しました。

- base-ogen3-dev　→ com.ogen3.base-ogen3.dev
- base-ogen3-stg　→ com.ogen3.base-ogen3.stg
- base-ogen3-prod　→ com.ogen3.base-ogen3.prod

そして、それぞれの`GoogleService-Info.plist`をダウンロードしてきて、ファイル名をそれぞれ以下に変更します。

- base-ogen3-dev　→ GoogleService-Info-prod.plist
- base-ogen3-stg　→ GoogleService-Info-stg.plist
- base-ogen3-prod　→ GoogleService-Info-dev.plist

ここまで用意できたら、Firebaseから一旦離れ、Xcodeに移ります。

###Xcode側の設定をする

まずは、Xcodeを開く（画像はAndroid Studioから開いています）
![open xcode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/7eaa7253-b01a-b742-7457-97da8605d783.png)

Firebaseという名前のgroupを作成し、続いて`Add Files to "Runner"...`を選択
![create firebase group.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/549f6165-80e4-9d63-6141-b7f8b28aeec3.png)

先ほど、ダウンロード＆改名しておいた`GoogleService-Info(-dev, -stg, -prod).plist` たちを追加します。
※ `Add to targets` にチェックも忘れずに
![add google service info.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/9c44e6ca-679c-6d1a-dd89-608a2411d21b.png)

次に、Firebase配下ではなく、Runner配下に`GoogleService-Info.plist`を追加する。
これは後ほどの設定で、`GoogleService-Info(-dev, -stg, -prod).plist` のどれかに置き換わるので、ファイル名さえ`GoogleService-Info.plist`になっていれば中身がなくてもよいみたいですが、今回は`base-ogen3-dev`（Firebaseプロジェクト）のものを新しくダウンロードして、改名せず配置しました。
![target google service.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/967f83ee-e58a-c9db-5d06-e1cdc7cb9798.png)

ここからは`GoogleService-Info.plist` を環境に合わせて変更できるようにしていきます。

まずは、`base_ogen3/ios`配下に `replace_google_service_info`というファイルを作成

```
$ touch ios/replace_google_service_info.sh
```

また、ビルド時にパーミッションの問題が発生することがあるので、以下しておく。

```
$ chmod 755 ios/replace_google_service_info.sh
```

中身を以下のようにする。
`DEFINE_BUILD_ENV`の部分は `--dart-define`で渡す値のキーに相当
（例: --dart-define=DEFINE_BUILD_ENV=dev）

```base_ogen3/ios/replace_google_service_info.sh
#! /bin/bash

if [[ $DEFINE_BUILD_ENV == *"dev"* ]]; then
  cp $PRODUCT_NAME/Firebase/GoogleService-Info-dev.plist $PRODUCT_NAME/GoogleService-Info.plist
elif [[ $DEFINE_BUILD_ENV == *"stg"* ]]; then
  cp $PRODUCT_NAME/Firebase/GoogleService-Info-stg.plist $PRODUCT_NAME/GoogleService-Info.plist
elif [[ $DEFINE_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 $DEFINE_BUILD_ENV
  exit 1
fi
```

ファイルを作成したら、`Build Phases`タブを開き、`＋`ボタンから`New Run Script Phase`を選択
![Screen Shot 2021-08-25 at 8.20.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/331f711f-3838-cf8d-3fb5-18aadbeecb6d.png)

追加された`Run Script`の Shellのところに`./replase_google_service_info.sh`を記入
また、`Output Files`に`＋`ボタンで`$SRCROOT/Runner/GoogleService-Info.plist`を記入
![Screen Shot 2021-08-25 at 8.21.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/beca9903-d0b8-ac1d-e02a-9f6e3620a762.png)

`Run Script`というタイトルをわかりやすいように`Replace Google Service Info`にリネームして、`Copy Bundle Resources`より上に配置
![Screen Shot 2021-08-25 at 8.28.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/fd4cb70d-ed2b-6868-ff42-9f5ff22fa53b.png)

次は、`--dart-difine=XXX=YYY`で渡された値をXcode側で使えるようにします。
まずは`Flutter`配下に`New File...`
![Screen Shot 2021-08-27 at 8.07.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/ceaf3525-3ea5-233b-57cc-d29ce85591c3.png)

`Configuration Setting File`を選択して、`Next`
![Screen Shot 2021-08-27 at 8.07.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/6171757c-0768-6b3f-e77a-54f1d63e4bea.png)

ファイル名を`DartDefineDefaults`として`Create`
※Folderを`Flutter`にする
※Groupを`Flutter`にする
※Targetsにチェックを入れる
※拡張子は作成後に自動で`.xcconfig`としてくれる
![Screen Shot 2021-08-27 at 8.09.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/8bb65e01-9994-c8e7-01e3-63c833210f14.png)

作成したら、中身を以下にする

```
DEFINE_BUNDLE_ID_SUFFIX=.dev
```
これは最終的に、ビルド時に`--dart-define=DEFINE_BUNDLE_ID_SUFFIX=XXXX`として実行するが、これが実行時に指定されなかった時（もしくはタイポしてしまった時）などにXcode側でのデフォルト値として使えるようにするために用意しています。

![Screen Shot 2021-08-27 at 8.18.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/a2cb091b-271c-2526-8a68-bc318096e8b0.png)

`Debug.xccofig`を開いて、ファイルの下に以下の２行を追加します。
（この2行の順番は大事なので、`DartDefine.xcconfig`が下に来るように記述）

```Debug.xcconfig
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig".
#include "DartDefineDefaults.xcconfig" <=追加
#include "DartDefine.xcconfig"         <=追加
```
![Screen Shot 2021-08-28 at 13.00.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/e7852f79-43b6-12e8-9d97-9d55d05539f4.png)

`Release.xcconfig`も同様にコード追加
![Screen Shot 2021-08-28 at 13.00.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/ec5e0679-c9e8-7959-ffc0-1ed2973fc81a.png)

次に、`Build Settings`タブを開き、`Packaging`セクション内の`Product Bundle Identifier`のドロップダウンを開きます。
`Debug` `Profile` `Release`それぞれの値を編集で語尾に`$(DEFINE_BUNDLE_ID_SUFFIX)`を付け加えます。

この設定をすると、バンドルIDの語尾が`.dev`になるはずです。

(余談：参考画像では `com.example.baseOgen3.dev` となっていますが、これは冒頭に述べた今回用意したBundle ID（com.ogen3.base-ogen3.dev）とは異なっていることに気づかず少しハマりました。 ここは`com.ogen3.base-ogen3.dev`でした)
![Screen Shot 2021-08-28 at 13.06.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/3256113c-740c-3c20-a8f0-60e020b99fff.png)


続いて、`Product > Scheme > Edit Scheme...`を選択
![Screen Shot 2021-08-27 at 8.23.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/436f6d96-027d-bbf4-9d1e-0753c9b2e192.png)

サイドメニューの`Build`を開いて`Pre-actions`を選択し、下部にある`+`を選択して`New Run Script Action`
![Screen Shot 2021-08-27 at 8.25.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/83e7935a-64fa-c941-913f-7d74b41539ca.png)

`Provide build settings from`の部分を`Runner`にして
中身を以下にする
（ただし、ここのコードはFlutterのバージョンによって異なります。参考記事: [flutterのdart-defineは2.2以上ではbase64でエンコードされるので対応したスクリプトの紹介](https://qiita.com/mogmet/items/dd9191ddc7d43f13bd62)）

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

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


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

printf "%s\n" "${define_items[@]}"|grep '^DEFINE_' > ${SRCROOT}/Flutter/DartDefine.xcconfig

```
![Screen Shot 2021-08-27 at 8.26.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/3282b10a-8045-e5db-4b68-514da4ea9cad.png)

ここまでで準備が完了しました。あとはFlutterに戻り確認しましょう。

###動作確認

確認の準備をするために以下を`pubspec.yaml`に追加

```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  package_info: ^2.0.2 　　 #<= 追加
  firebase_core: ^1.5.0    #<= 追加
  cloud_firestore: ^2.5.0  #<= 追加

```

```
$ flutter clean && flutter pub get
```

[FlutterFireのOverView](https://firebase.flutter.dev/docs/overview)を参考にFirebaseをFlutterにセットアップ
`main.dart`を編集します

####Firestoreのデータを表示するコード追加
```main.dart
import 'package:firebase_core/firebase_core.dart'; //追記
import 'package:flutter/material.dart';

void main() async { // async を追記
  WidgetsFlutterBinding.ensureInitialized(); //追記
  await Firebase.initializeApp(); //追記
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
```

次にFirebaseからデータを取得するコード追加します。
今回は[FlutterFireの One-time Readのサンプルコード](https://firebase.flutter.dev/docs/firestore/usage/#one-time-read)をそのまま利用します。
追加する場所はもちろんどこでもいいですが、ここでは`main.dart`の一番下に追記していきます

```main.dart
// この上に _MyHomePageState クラスがあるはず（flutter create で自動で生成されるやつ）
// ここから下に追加
class GetUserName extends StatelessWidget {
  final String documentId;

  GetUserName(this.documentId);

  @override
  Widget build(BuildContext context) {
    CollectionReference users = FirebaseFirestore.instance.collection('users');

    return FutureBuilder<DocumentSnapshot>(
      future: users.doc(documentId).get(),
      builder:
          (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {

        if (snapshot.hasError) {
          return Text("Something went wrong");
        }

        if (snapshot.hasData && !snapshot.data!.exists) {
          return Text("Document does not exist");
        }

        if (snapshot.connectionState == ConnectionState.done) {
          Map<String, dynamic> data = snapshot.data!.data() as Map<String, dynamic>;
          return Text("Full Name: ${data['full_name']} ${data['last_name']}");
        }

        return Text("loading");
      },
    );
  }
}
```

上記のコードではFirestoreの`users`というコレクションから引数で受け取った`documentId`でデータを取得して、そのドキュメントの`full_name`と`last_name`というフィールドのデータを表示するコードになっているので、これに合わせて自分のFirestoreも作成します。

####Firestoreにサンプルデータを作成
こんな感じになると思います。(ドキュメントIDは`abcd`としました。)
また下記画像は`-dev`のプロジェクトのものですが、同じように`-stg`と`-prod`のfirebaseプロジェクトにもデータを用意します。
ただし、`full_name`と`last_name`の値の語尾はそれぞれの環境を表すように`.dev`、`.stg`、`.prod`としています。
![Screen Shot 2021-08-27 at 9.03.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/53771420-2586-bf4f-2bf3-9ac2e32b8909.png)

またFirestoreのセキュリティルールもデータを取得できるように以下に変更しておきます。

```
allow read, write: if true;
```
注意：この記述は簡易的にデータの取得を確認するために書いています。本当のサービスなどで利用するには危険だと思われるので、本格的に運用するときは、しっかりとしたセキュリティルールを書くようにしてください。
![Screen Shot 2021-08-27 at 9.06.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/ba7fd66a-e8e5-3a73-d845-603a569ec9fd.png)

Flutterに戻り、`main.dart`で先ほど追加した`GetUserName`ウィジェットを表示できるよう編集します。（FutureBuilderの部分はFirebaseでなく `package_info` のコードです。）
参考画像ではGetUserNameの引数が`'abcdefg'`となっていますが、Firestoreに用意したドキュメントIDは`'abcd'`だったので、後者にしてください。


```main.dart
          // _MyHomePageState内
          // ここから追記
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
          children: <Widget>[
            FutureBuilder<PackageInfo>(
                future: PackageInfo.fromPlatform(),
                builder: (context, value) {
                  if (!value.hasData) {
                    return Container();
                  }
                  return Text(
                    value.data?.packageName ?? 'null value',
                    style: Theme.of(context).textTheme.headline6,
                  );
                }
            ),
            const SizedBox(height: 50),  
            GetUserName('abcdefg'),
            const SizedBox(height: 50),
            Text(
              'You have pushed the button this many times:',
            ),
            // ここまでが追記
            Text(
```

Xcodeを開き、Runner > PROJECT Runner を選択し、 Infoタブにて `iOS Deployment Target`を `11.0`にする
![Screen Shot 2021-08-28 at 11.55.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/2cb043e0-f1d4-7e55-598c-792ca8b49415.png)


念の為`Podfile`内の `platform :ios, '11.0'`にしておく

![Screen Shot 2021-08-28 at 11.56.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/fd24b9ee-cf56-69fb-6d60-98f52775fa92.png)

これで準備は全て完了です。実行しましょう。

###実行
（`--dart-define`の値やビルドモードは好きな組み合わせで試してください）

```
$ flutter run --debug --dart-define=DEFINE_BUNDLE_ID_SUFFIX=.dev --dart-define=DEFINE_BUILD_ENV=dev
```

画面に`--dart-define`で指定した環境（Firebaseプロジェクト）からデータを取得できているのがわかると思います。

![Screen Shot 2021-08-28 at 15.48.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631713/009734ee-6711-f2d9-2db3-893ba065c9bd.png)

以上です。

##まとめ & 感想

この環境分けに取り組み始めた時はとてもややこしく感じたが、やろうとしていることを大きく捉えると以下の２工程かなと思い、これを意識しながらやるとなんとかできた。
1. FlutterからXcode側に`dart-define`で値を渡す。
2. Xcode側で受け取った値を使えるようにし、それらを使って`Bundle ID`やら`GoogleService-Info.plit`やらを変えていく

個人的には --flavor を使った環境分けよりXcodeでのScheme設定設定が少なくていいなと思った。

最後までお読みいただきありがとうございました。

##参考記事
[Flutterで環境ごとにビルド設定を切り替える — iOS編](https://medium.com/flutter-jp/flavor-b952f2d05b5d)
[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)
[dart-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける iOS編](https://qiita.com/tetsufe/items/3f2257ac12f812d3f2d6)
[flutterのdart-defineは2.2以上ではbase64でエンコードされるので対応したスクリプトの紹介](https://qiita.com/mogmet/items/dd9191ddc7d43f13bd62)
[FlutterからXcodeへ環境変数を渡す](https://qiita.com/yana1316/items/542dd4f787779d154f45)
[Flutterで本番/開発 別に切り分けてアプリを入れる](https://qiita.com/yana1316/items/12b9de7149e1fcae1735)