##はじめに
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で本番/開発 別に切り分けてアプリを入れる