はじめに
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で分けたい環境の数だけプロジェクトを作成する。
今回は3つ作ります
(プロジェクト名はなんでもよい。ただし、語尾を「-dev」のようにすると環境がわかりやすい)
- base-ogen3-dev
- base-ogen3-stg
- base-ogen3-prod
次にそれぞれに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から開いています)

Firebaseという名前のgroupを作成し、続いてAdd Files to "Runner"...を選択

先ほど、ダウンロード&改名しておいたGoogleService-Info(-dev, -stg, -prod).plist たちを追加します。
※ Add to targets にチェックも忘れずに

次に、Firebase配下ではなく、Runner配下にGoogleService-Info.plistを追加する。
これは後ほどの設定で、GoogleService-Info(-dev, -stg, -prod).plist のどれかに置き換わるので、ファイル名さえGoogleService-Info.plistになっていれば中身がなくてもよいみたいですが、今回はbase-ogen3-dev(Firebaseプロジェクト)のものを新しくダウンロードして、改名せず配置しました。

ここからは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)
# ! /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を選択

追加されたRun Scriptの Shellのところに./replase_google_service_info.shを記入
また、Output Filesに+ボタンで$SRCROOT/Runner/GoogleService-Info.plistを記入

Run ScriptというタイトルをわかりやすいようにReplace Google Service Infoにリネームして、Copy Bundle Resourcesより上に配置

次は、--dart-difine=XXX=YYYで渡された値をXcode側で使えるようにします。
まずはFlutter配下にNew File...

Configuration Setting Fileを選択して、Next

ファイル名をDartDefineDefaultsとしてCreate
※FolderをFlutterにする
※GroupをFlutterにする
※Targetsにチェックを入れる
※拡張子は作成後に自動で.xcconfigとしてくれる

作成したら、中身を以下にする
DEFINE_BUNDLE_ID_SUFFIX=.dev
これは最終的に、ビルド時に--dart-define=DEFINE_BUNDLE_ID_SUFFIX=XXXXとして実行するが、これが実行時に指定されなかった時(もしくはタイポしてしまった時)などにXcode側でのデフォルト値として使えるようにするために用意しています。
Debug.xccofigを開いて、ファイルの下に以下の2行を追加します。
(この2行の順番は大事なので、DartDefine.xcconfigが下に来るように記述)
# include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
# include "Generated.xcconfig".
# include "DartDefineDefaults.xcconfig" <=追加
# include "DartDefine.xcconfig" <=追加
次に、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でした)

続いて、Product > Scheme > Edit Scheme...を選択

サイドメニューのBuildを開いてPre-actionsを選択し、下部にある+を選択してNew Run Script Action

Provide build settings fromの部分をRunnerにして
中身を以下にする
(ただし、ここのコードはFlutterのバージョンによって異なります。参考記事: flutterのdart-defineは2.2以上ではbase64でエンコードされるので対応したスクリプトの紹介)
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
ここまでで準備が完了しました。あとはFlutterに戻り確認しましょう。
動作確認
確認の準備をするために以下を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を参考にFirebaseをFlutterにセットアップ
main.dartを編集します
Firestoreのデータを表示するコード追加
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のサンプルコードをそのまま利用します。
追加する場所はもちろんどこでもいいですが、ここでは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としています。

またFirestoreのセキュリティルールもデータを取得できるように以下に変更しておきます。
allow read, write: if true;
注意:この記述は簡易的にデータの取得を確認するために書いています。本当のサービスなどで利用するには危険だと思われるので、本格的に運用するときは、しっかりとしたセキュリティルールを書くようにしてください。

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

念の為Podfile内の platform :ios, '11.0'にしておく
これで準備は全て完了です。実行しましょう。
実行
(--dart-defineの値やビルドモードは好きな組み合わせで試してください)
$ flutter run --debug --dart-define=DEFINE_BUNDLE_ID_SUFFIX=.dev --dart-define=DEFINE_BUILD_ENV=dev
画面に--dart-defineで指定した環境(Firebaseプロジェクト)からデータを取得できているのがわかると思います。
以上です。
まとめ & 感想
この環境分けに取り組み始めた時はとてもややこしく感じたが、やろうとしていることを大きく捉えると以下の2工程かなと思い、これを意識しながらやるとなんとかできた。
- FlutterからXcode側に
dart-defineで値を渡す。 - Xcode側で受け取った値を使えるようにし、それらを使って
Bundle IDやらGoogleService-Info.plitやらを変えていく
個人的には --flavor を使った環境分けよりXcodeでのScheme設定設定が少なくていいなと思った。
最後までお読みいただきありがとうございました。
参考記事
Flutterで環境ごとにビルド設定を切り替える — iOS編
Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything
dart-defineでFlutterアプリのFirebase開発環境と本番環境を使い分ける iOS編
flutterのdart-defineは2.2以上ではbase64でエンコードされるので対応したスクリプトの紹介
FlutterからXcodeへ環境変数を渡す
Flutterで本番/開発 別に切り分けてアプリを入れる






