複数Firebaseプロジェクト対応!Flutterの環境別構成ファイル管理
はじめに
こんにちは、スペースマーケットでモバイルアプリの開発を楽しんでいる王です。
最近気温変化が激しく風邪気味ですが、皆さんも体調管理しっかりしましょう。
弊社のホスト向けアプリ「SPACEMARKET for HOST」はOAuth認証の技術負債返却において、
環境ごとに複数のFirebase Projectを追加する必要がありました。
今回は環境ごとに複数のFirebase Projectを切り替える方法について紹介したいと思います。
環境
- Flutter 3.13.9
- Dart 3.1.5
本記事で紹介しないこと
- Firebaseコンソール上の設定
開発環境を分ける
Flutterでは開発環境を複数に分けて構築する方法がいくつかありますが、
本記事では--dart-define-from-file
オプションを使用する方法をご紹介します。
まず、環境ごとに変数をまとめるファイルを作成し、必要な項目を定義します。
DEFINE_BUILD_ENV=
で環境を指定します。
ファイル名と配置場所は自由に設定できますが、プロジェクト内での統一を意識しましょう!
onesignalAppId="xxxx-xxxx-xxxx-xxxx"
appName="SPACEMARKET for HOST debug"
androidIcon="@mipmap/debug_launcher_icon"
DEFINE_BUILD_ENV=dev
onesignalAppId="xxxx-xxxx-xxxx-xxxx"
appName="SPACEMARKET for HOST"
androidIcon="@mipmap/launcher_icon"
DEFINE_BUILD_ENV=prod
そして、flutter run/buildコマンドに--dart-define-from-file
というオプションをつけることで環境を指定することができます。
# dev環境でflutterアプリを起動する
flutter run --dart-define-from-file=dart_defines/dev.env
これで環境を分けることができました。
準備
Firebaseを使うための準備は公式ドキュメントを参考にしてください。
ただ、複数のFirebase Projectが必要なので、
環境×Firebase Projectの数×プラットフォームの構成ファイル
- iOS:
GoogleService-Info.plist
- Android:
google-services.json
が必要です。
今回は2つのFirebase Projectが必要なので、
GoogleService-Info.plist
とgoogle-services.json
それぞれ4つダウンロードする必要があります。
やり方
共通
iOS/Androidは共通なので、先に説明します。
iOS/Androidの配下にdevとprodそれぞれの構成ファイルをおいて、ビルドする時に環境を見て正しい構成ファイルをコピーする
という力技で実現します。
例として、自分のファイル構成が以下になります。
iOS
├── GoogleService-Info.plist(コピーされた構成ファイル)
├── GoogleService-Info-Auth.plist(コピーされた構成ファイル)
└── FirebaseConfig
├── dev
│ ├── GoogleService-Info-Dev.plist
│ └── GoogleService-Info-Dev-Auth.plist
└── prod
├── GoogleService-Info-Prod.plist
└── GoogleService-Info-Prod-Auth.plist
Android
└── app
├── google-services.json(コピーされた構成ファイル)
├── google-services-auth.json(コピーされた構成ファイル)
└── src
└── config
├── dev
│ └── google-services-auth.json
└── prod
├── google-services.json // google-servicesは共通なので一個のみ
└── google-services-auth.json
iOS側
まずコピー用のスクリプトを書いていきます
function base64decode() { echo "${*}" | base64 --decode; }
# IFS でカンマ区切りの値を配列として読み込む
IFS=',' read -r -a define_items <<< "$DART_DEFINES"
# 各項目をデコード
for index in "${!define_items[@]}"
do
define_items[$index]=$(base64decode "${define_items[$index]}")
done
# DEFINE_BUILD_ENV の値を抽出する
DEFINE_BUILD_ENV=""
for item in "${define_items[@]}"; do
if [[ $item == DEFINE_BUILD_ENV* ]]; then
DEFINE_BUILD_ENV=$(echo $item | cut -d'=' -f2)
fi
done
# DEFINE_BUILD_ENV の値で判断してファイルをコピー
if [[ $DEFINE_BUILD_ENV == *"dev"* ]]; then
cp ${SRCROOT}/FirebaseConfig/dev/GoogleService-Info-Auth-Dev.plist ${SRCROOT}/GoogleService-Info-Auth.plist
cp ${SRCROOT}/FirebaseConfig/dev/GoogleService-Info-Dev.plist ${SRCROOT}/GoogleService-Info.plist
echo "Your DEFINE_BUILD_ENV is '$DEFINE_BUILD_ENV'"
elif [[ $DEFINE_BUILD_ENV == *"prod"* ]]; then
cp ${SRCROOT}/FirebaseConfig/prod/GoogleService-Info-Auth-Prod.plist ${SRCROOT}/GoogleService-Info-Auth.plist
cp ${SRCROOT}/FirebaseConfig/prod/GoogleService-Info-Prod.plist ${SRCROOT}/GoogleService-Info.plist
echo "Your DEFINE_BUILD_ENV is '$DEFINE_BUILD_ENV'"
else
echo "configuration didn't match to 'dev' or 'prod'"
echo "Your DEFINE_BUILD_ENV is '$DEFINE_BUILD_ENV'"
exit 1
fi
そしてこのスクリプトを以下の箇所に追加します。
ビルドする前に構成ファイルをコピーする必要があるため、Project Targets -> Build PhasesではなくEdit Schemaのpre-actionsに追加。
また、Archiveへの追加が漏れると実際にアーカイブしたアプリが動かなくなるので絶対に追加してください!
Android側
android/app/build.gradleへ以下のコードを追加
def dartEnvironmentVariables = [:]
if (project.hasProperty('dart-defines')) {
dartEnvironmentVariables = dartEnvironmentVariables + project.property('dart-defines')
.split(',')
.collectEntries { entry ->
def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
[(pair.first()): pair.last()]
}
}
// Firebase構成ファイルを android/app ディレクトリにコピーします
task copyGoogleServicesAuthJson {
doFirst {
copy {
from "src/config/${dartEnvironmentVariables.DEFINE_BUILD_ENV}/google-services-auth.json"
into './'
}
}
}
// google-services.jsonはdev/prodで共通のため、コピーして android/app ディレクトリに移動
def googleServicesJson = "src/config/prod/google-services.json"
task copyGoogleServicesJson {
doFirst {
copy {
from "src/config/prod/google-services.json"
into './'
}
}
}
// firebaseファイルのコピータスクを待ってからファイルが必要な処理を行うよう設定
tasks.configureEach { task ->
if (task.name == 'processDebugGoogleServices') {
task.dependsOn copyGoogleServicesJson
task.dependsOn copyGoogleServicesAuthJson
}
if (task.name == 'processReleaseGoogleServices') {
task.dependsOn copyGoogleServicesJson
task.dependsOn copyGoogleServicesAuthJson
}
}
感想
今回の取り組みで一番面倒だったのは、Flutter側から環境ごとの構成ファイルを動的に指定できないことでした。
ネイティブであれば、例えばアプリの初期化時に環境変数を読み取り、適切な構成ファイルをファイル名で指定して読み込むことができます。
ところが、Flutterにはそのような機構が存在しません。(このタスク自体が2024/11月でやったので今ならあったかもしれません)
さらにやっかいなのが、なぜか自分の環境ではビルド前に構成ファイルが正しい場所に存在していないと、そもそもビルド自体が失敗するという点です。
ネイティブコード側に処理を書くことも検討しましたが、Firebaseの初期化処理がアプリのライフサイクルよりも早く走るため、アプリ起動時にファイルを差し替えるのでは遅すぎるという制約がありました🫠
そのため、最終的には「ビルド前に必要な構成ファイルをコピーしておく」という力技に落ち着きました。
この制約があることで、CI/CDやローカルビルドにおいても、ファイルコピーの処理を常に忘れず仕込んでおく必要があります。
最後に
今回は、Flutterで環境ごとに設定を切り替える方法と、Firebase構成ファイルの管理手法について紹介しました。
ファイルをコピーして使うという手法で目的を実現しましたが、もっといいやり方がありましたら、ぜひコメントで教えていただけますと幸いです。
参考

スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion