Flutter

【Flutter】Firebaseの本番環境と開発環境を構築する方法を細かく解説

FirebaseのCloud FirestoreやCloud Functionsを利用する場合、開発環境でセキュリティールールやFunctionsの動作を確認してから、本番に適用したくなります。

そこで、この記事では、debugビルドでは開発環境用の、releaseビルドでは本番環境用のFirebaseプロジェクトに繋がるようにする方法を丁寧に解説します。

実行環境は

  • MacBook Pro M1 Max
  • macOS Ventura 13.5.2
  • Flutter3.13.9
  • AndroidStudio Giraffe | 2022.3.1 Patch 2
  • XCode 15.0.1

です。

Mac環境で作業を進めますが、Windowsでも作業内容は一緒だと思います。

 

事前準備

新規作成したプロジェクトに対応したFirebaseプロジェクトを、flutterfire_cliというライブラリを使って、自動的に作成していきます。

そのため、まずはAndroidStudioのTerminalタブを選んで次のコマンドを実行して、Firebaseにログインしておきます。

firebase login

 

次に、以下のコマンドを実行して、flutterfire_cliをインストールします。

dart pub global activate flutterfire_cli

 

 

Firebaseの導入

flutterfire_cliを使って一気に、必要なプラットフォームに対応したFirebaseプロジェクトの作成と設定ファイルのダウンロードを行います。

今回はiOSとAndroidのプロジェクトを作成します。

AndroidStudioのTerminalタブを選んで次のコマンドを実行します。

flutterfire configure

? Select a Firebase project to configure your Flutter application with ›
と出るので
<create a new project>
を選択してEnter

? Enter a project id for your new Firebase project (e.g. my-cool-project)
と出るので、プロジェクト名を入力してEnter。
今回は「prod-and-test」と入力しました。

世界中の誰かが既に付けたプロジェクト名を指定するとエラーがでるようです。

 

次に、今作成されたFirebaseプロジェクトに紐付けるプラットフォームが聞かれるので、iosとandroidを選択します。

? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web

IMEが全角になっていると環境を選択するSpace操作が効かないので、必ず半角にしておいてください。

何か聞かれるので、デフォルトの「yes」でEnter

? The files android/build.gradle & android/app/build.gradle will be updated to apply Firebase configuration and gradle build plugins. Do you want to continue? (y/n) › yes

これで、自動的に

  • Firebaseプロジェクトの作成
  • iOSアプリとAndroidアプリの紐づけ
  • 設定ファイルのダウンロード
    • iOSは、[root]/ios/Runner/GoogleService-Info.plist
    • Androidは、[root]/android/app/google-services.json
  • Firebaseの設定dartファイルの追加
    • [root]/lib/firebase_options.dart

が完了します。

この段階で、firebase_coreライブラリが入っていないとエラーになるのに自動で入らないので、AndroidStudioのTerminalタブで次のコマンドを実行して追加します。

flutter pub add firebase_core

これでビルドが通るようになります。

 

Test環境用のFirebaseプロジェクトの作成

先程、flutterfire_cliで自動作成したFirebaseプロジェクトは本番環境用という事にして、開発環境用のFirebaseプロジェクトを別途手動で作成します。

開発環境用のFirebaseプロジェクトを作成したら、Flutterマークのボタンを「押さず」、iOSとAndroidマークを押して個別にFirebaseプロジェクトに紐付けるアプリを追加してください。

  • iOSのバンドルIDや、Androidのパッケージ名は、本番環境と同じものを入れてください。
  • ライブラリーの導入や初期化設定は、Flutter側で行うので無視してください。
  • 設定ファイル、GoogleService-Info.plistやgoogle-services.jsonは、後ほど配置方法を説明するので、ひとまずダウンロードして控えておいてください。

Flutterマークのボタンを押して、指示に従ってflutterfire_cliのコマンドを実行していくと、先程作成した本番環境用の設定が上書きされてしまうので気をつけてください。

完了。

 

Firebaseの初期化処理を追加

Firebaseの初期化処理を追加します。

import 'package:flutter/material.dart';

import 'package:firebase_core/firebase_core.dart';  // 追加
import 'firebase_options.dart'; // 追加


void main() async {   // asyncを追加
  // 設定の初期化を待たせる
  WidgetsFlutterBinding.ensureInitialized();  // 追加

  // Firebaseの初期化処理を追加
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  
  runApp(const MyApp());
}

....

ここまでで「本番環境のFirebaseプロジェクトに接続する」設定は完了です。
念のため実行してみて、エラーが出た場合はここまでの手順を確認してください。

 

ビルドに合わせて接続先が切り替わるよう修正

flutterfire_cliが自動で追加したファイル、[root]/lib/firebase_options.dart、を開くと下の方に次のようなFirebaseの設定変数が2つあります。

static const FirebaseOptions android = FirebaseOptions(
    apiKey: 'AtakadesuyoPOUUstQ1EDnZIRzq9rJ_96VK0YRwjU',
    appId: '1:12345678910:android:31b0b29867d2b236e05408',
    messagingSenderId: '123456789',
    projectId: 'prod-and-test',
    storageBucket: 'prod-and-test.appspot.com',
  );

static const FirebaseOptions ios = FirebaseOptions(
    apiKey: 'AtakadesuyNUMrb10s0JkfEVND1Fxapt-aDN82iY',
    appId: '1:12345678910:ios:afffd4cb82a32ac5e05408',
    messagingSenderId: '12345678910',
    projectId: 'prod-and-test',
    storageBucket: 'prod-and-test.appspot.com',
    iosBundleId: 'jp.toconakis.foo',
  );

 

これらをコピー&ペーストして、それぞれandroid_dev, ios_devとリネームした後、apiKeyやappIddなど各パラメータを、先程ダウンロードした開発環境用Firebaseプロジェクトの設定ファイル、GoogleService-Info.plist (iOS用)、google-services.json (Android用)の内容を見ながら書き換えます。

....


// 元々あったパラメータ
static const FirebaseOptions android = FirebaseOptions(
    apiKey: 'AtakadesuyoPOUUstQ1EDnZIRzq9rJ_96VK0YRwjU',
    appId: '1:12345678910:android:31b0b29867d2b236e05408',
    messagingSenderId: '123456789',
    projectId: 'prod-and-test',
    storageBucket: 'prod-and-test.appspot.com',
  );

static const FirebaseOptions ios = FirebaseOptions(
    apiKey: 'AtakadesuyNUMrb10s0JkfEVND1Fxapt-aDN82iY',
    appId: '1:12345678910:ios:afffd4cb82a32ac5e05408',
    messagingSenderId: '12345678910',
    projectId: 'prod-and-test',
    storageBucket: 'prod-and-test.appspot.com',
    iosBundleId: 'jp.toconakis.foo',
  );


// 以下を追加
static const FirebaseOptions android_dev = FirebaseOptions(
    apiKey: 'Atakadesu1OI1Kil-xCjph003R_BpeiRhBkstzlk',   // client > api_key > current_key
    appId: '1:777777777:android:12345678f3acc7b669014b',  // client > client_info > mobilesdk_app_id
    messagingSenderId: '23456789101112', // project_info > project_number
    projectId: 'test-prod-and-test', // project_info > project_id
    storageBucket: 'test-prod-and-test.appspot.com', // project_info > storage_bucket
  );

static const FirebaseOptions ios_dev = FirebaseOptions(
    apiKey: 'Atakadesugh8oyw7bP1GcvkZGJPz8NhLR8uiJ4', // API_KEY
    appId: '1:6666666666:ios:12345678aa881113069014b',  // GOOGLE_APP_ID
    messagingSenderId: '5678910111213',  // GCM_SENDER_ID
    projectId: 'test-prod-and-test', // PROJECT_ID
    storageBucket: 'test-prod-and-test.appspot.com', // STORAGE_BUCKET
    iosBundleId: 'jp.toconakis.foo',  // 変更なし
  );

 

そして、firebase_options.dart、の最初の方を、ビルド環境に合わせて、返すパラメータを切り替えるようにします。

class DefaultFirebaseOptions {
  // ビルド環境の判定メソッドを追加
  static bool isRelease() {
    return const bool.fromEnvironment('dart.vm.product');
  }

  static FirebaseOptions get currentPlatform {
    if (kIsWeb) {
      throw UnsupportedError(
        'DefaultFirebaseOptions have not been configured for web - '
            'you can reconfigure this by running the FlutterFire CLI again.',
      );
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        // ビルド環境に合わせて分岐するように変更
        if (isRelease()) {
          // 本番環境
          return android;
        } else {
          // 開発環境
          return android_dev;
        }

      case TargetPlatform.iOS:
        // ビルド環境に合わせて分岐するように変更
        if (isRelease()) {
          // 本番環境
          return ios;
        } else {
          // 開発環境
          return ios_dev;
        }

.....

 

最後に、アプリが読み込むFirebaseプロジェクトの設定ファイルがビルド環境に合わせて自動で切り替わるようにします。iOSとAndroidでやり方が違うのでそれぞれを説明していきます。

Androidの場合

Androidは簡単です。

[root]/android/app、にreleaseとdebug、2つのフォルダーを作成して、本番環境用のgoogle-services.jsonをreleaseフォルダに、開発環境用はdebugフォルダに配置したら完了です。

 

iOSの場合

iOSはちょっと手間が掛かります。

[root]/ios、にある本番環境用のGoogleService-Info.plistは、GoogleService-Info-release.plist、とリネームしておきます。

そして開発環境用にダウンロードしておいたGoogleService-Info.plistも「同じフィルダーに」移した後、GoogleService-Info-dev.plist、とリネームしておきます。

 

そして、XCodeでビルドする時に、環境に合わせて適切なplistが選ばれるよう、シェルを実行させる設定を追加します。

[root]/ios/Runner/Runner.xcworkspace、をXCodeで開きます。

開くと、先程GoogleService-Info.plistをリネームしたせいで参照が切れて赤く表示されているので、削除します。

そして、GoogleService-Info-release.plistとGoogleService-Info-dev.plistをドラッグ&ドロップして追加しておきます。

Create groupsを選んで、targetsはRunnerを選んでFinish

で、こうなる。

その後、TARGETSからBuild Phasesタブを選び、+ボタンを押したら、New Run Script Phaseを押します。

すると、下にRun Scriptという項目が追加されるので、以下のシェルを追加します。

# まずGoogleService-Info.plistを削除して
rm -rf "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"

if [ "${CONFIGURATION}" = "Debug" ]  ; then
    # 開発ビルドの場合は、GoogleService-Info-devをコピーして、GoogleService-Infoにリネームして配置する
  cp "$SRCROOT/GoogleService-Info-dev.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"

elif [ "${CONFIGURATION}" = "Release" ] ; then
    # 本番ビルドの場合は、GoogleService-Info-releaseをコピーして、GoogleService-Infoにリネームして配置する
  cp "$SRCROOT/GoogleService-Info-release.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
fi

 

こんな感じ。

これで、iOSアプリをビルドする時に、自動的に環境に合わせたGoogleService-Info.plistファイルが生成されて、リリースビルドでは本番環境のFirebaseプロジェクトに、開発ビルドでは開発環境のFirebaseプロジェクトに接続されるようになります。

XCode15.0.1でビルドしてみたら
createItemModels creation requirements should not create capability item model for a capability item model that already exists.
というエラーが出たので

[root]/ios/Podfile

を開いて下の方に次の設定を追記してください。
チームIDはApple Developerサイトの下の方、メンバーシップの詳細に書いています。

post_install do |installer|
  # ココから↓↓↓
  installer.generated_projects.each do |project|
    project.targets.each do |target|
      target.build_configurations.each do |config|
        config.build_settings["DEVELOPMENT_TEAM"] = "ここにチームIDを入れる"
      end
    end
  end
  # ココまで↑↑↑↑を追加

  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end

参考:
https://developer.apple.com/forums/thread/737923

 

動作確認

最後に、ちゃんとFirebaseプロジェクトが環境に合わせて切り替わっているかを、Firestoreに入っているデータを読み込んで表示させることで確認してみます。

本番用のFirebaseプロジェクトのFirestoreに、infoドキュメントを作成してenvフィールドに「production」という文字列を登録します。

開発用のFirebaseプロジェクトには、同じ構造でenvフィールドには「development」と登録しておきます。

このデータを読み込んで、本番環境ならproduction、開発環境ならdevelopmentと表示されることを確認していきます。

 

AndroidStudioのTerminalタブで次のコマンドを実行してfirestoreのインストール。

flutter pub add cloud_firestore

 

後は、firestoreを読み込んでTextに表示させます。

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:cloud_firestore/cloud_firestore.dart';


void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(MyApp());
}


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(body:
      Center(child:EnvWidget()),
    ));
  }
}


class EnvWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: FirebaseFirestore.instance.collection('info').snapshots(),
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
        return Text('env: ${snapshot.data?.docs[0]['env']}');
      },
    );
  }
}

 

結果はこの通り (Android)

Android環境でビルドした場合

The number of method references in a .dex file cannot exceed 64K.

というエラーが起こりました。

miniSdkVersionが21 (Android5.0)未満の場合、メソッドの総数は 65,536個に制限されるのですが、firestoreを導入したことで、その限界を超えちゃうようです。

今回使用したFlutter3.13.9は、miniSdkVersionに19が指定されてしまいます。

そこで、モジュールレベルのbuild.gradle (android/app/build.gradle)のminSdkVersionに、29 (Android10)あたりを指定すればOKです。

defaultConfig {
        applicationId "jp.toconakis.foo"
        minSdkVersion 29  // ここを変更
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }