🐙

Flutter + Firebase: Android特有の自動初期化問題とその解決

に公開

TL;DR

Flutter開発中に遭遇した、iOS Simulatorでは正常に動作するのにAndroid EmulatorでFirebase Emulatorに接続できない問題の解決記録です。

  • 問題: Android EmulatorでFirebase Emulatorに接続できない([core/duplicate-app] エラーなど)
  • 原因: google-servicesプラグインによるAndroid特有のネイティブ自動初期化
  • 解決: ネイティブ自動初期化の無効化とDart側での完全制御

背景

Firebase AuthとCloud Functionsを使用したFlutterアプリを開発中、開発環境でFirebase Emulatorに接続してテストしていました。

環境:

問題: Android EmulatorでFirebase Emulatorに接続できない

症状

Firebase Emulatorに接続しようとすると、様々なエラーが発生:

[core/duplicate-app] A Firebase App named "[DEFAULT]" already exists
E/RecaptchaCallWrapper: Initial task failed for action RecaptchaAction(action=signUpPassword)
with exception - An internal error has occurred. [ BLOCKING_FUNCTION_ERROR_RESPONSE:...]

さらに、Callable関数を呼び出すとNot Foundエラーも発生:

Firebase Functions (404 Not Found)

不思議な点:

  • ✅ iOS Simulator + Firebase Emulator: 問題なく動作
  • ✅ Android Emulator + リモート環境: 問題なく動作
  • ❌ Android Emulator + Firebase Emulator: 接続エラー、Callable関数エラー
  • 同じDartコード、同じFirebase初期化ロジックなのに...

つまり、Android Emulatorで開発用のFirebase Emulatorに接続するときだけ様々なエラーが発生し、正常に動作しませんでした。

原因調査

最初は以下を疑いました:

  1. ホットリロード時の初期化? → Firebase.apps.isEmptyチェック追加でも解決せず
  2. エミュレータのホスト設定? → localhost vs 10.0.2.2 の問題でもなかった

真の原因:

Androidではgoogle-servicesプラグインがアプリ起動時にネイティブレイヤーで自動的にFirebaseを初期化していた!

// android/app/build.gradle.kts
plugins {
    id("com.android.application")
    id("com.google.gms.google-services")  // ← これが犯人
    id("kotlin-android")
    id("dev.flutter.flutter-gradle-plugin")
}

何が問題だったのか:

  1. Android側(ネイティブレイヤー)でFirebaseAppが自動初期化される(google-servicesプラグイン)
  2. その後、Dart側でもFirebase.initializeApp()を呼び出す
  3. 結果としてFirebaseAppが二重に初期化される
  4. Dart側でエミュレータ接続設定(useAuthEmulatoruseFunctionsEmulator)を行っても、Android側の初期化設定で上書きされてしまい、Firebase Emulatorに接続できない
  5. Callable関数も本番環境への接続を試みるため404 Not Foundエラーになる

つまり、二重初期化による接続設定の上書きが根本的な問題でした。

解決策

1. google-servicesプラグインをコメントアウト

// android/app/build.gradle.kts
plugins {
    id("com.android.application")
    // google-servicesプラグインをコメントアウト - Dart側で明示的に初期化
    // id("com.google.gms.google-services")
    id("kotlin-android")
    id("dev.flutter.flutter-gradle-plugin")
}

2. AndroidManifest.xmlで自動初期化を無効化

<!-- android/app/src/main/AndroidManifest.xml -->
<application>
    <!-- Firebase自動初期化を無効化 - Dart側で明示的に初期化 -->
    <meta-data
        android:name="firebase_crashlytics_collection_enabled"
        android:value="false" />
    <meta-data
        android:name="firebase_analytics_collection_enabled"
        android:value="false" />
</application>

3. Dart側で完全制御

// lib/main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Firebase初期化 - ネイティブ自動初期化を無効化済み
  if (Firebase.apps.isEmpty) {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );
  }

  // Firebase Functions インスタンスの作成
  final functions = FirebaseFunctions.instanceFor(region: 'asia-northeast1');

  // FirebaseAuth インスタンスを取得
  final auth = FirebaseAuth.instance;

  // エミュレータ接続(開発環境)
  if (kDebugMode) {
    try {
      await auth.useAuthEmulator('localhost', 9099);
    } catch (e) {
      debugPrint('⚠️ Auth emulator already connected: $e');
    }
    functions.useFunctionsEmulator('localhost', 5001);
  }

  runApp(MyApp(auth: auth, functions: functions));
}

これでFirebase Emulatorに接続できるようになりました 🎉

別の解決策は可能か?

実験として、「Dart側の初期化を削除してネイティブ自動初期化に完全に任せる」アプローチも検討しました。

試したこと

  1. google-servicesプラグインを有効化(ネイティブ自動初期化ON)
  2. Dart側のFirebase.initializeApp()を削除

結果:不可能

問題点:

  • 一部のFirebaseツール(FirebaseUIAuthなど)はFirebase.initializeApp()の事前呼び出しが必須
  • ネイティブ自動初期化では認識されず、Dart側で明示的に呼ぶ必要がある
  • しかしDart側でFirebase.initializeApp()を呼ぶと二重初期化になる

結論:
ネイティブ自動初期化とDart側初期化はどちらか一方に統一する必要がある。現在の解決策(Dart側で完全制御)が最適解です。

学んだこと・ベストプラクティス

1. プラットフォーム依存のバグに注意

  • 同じDartコードでもネイティブレイヤーの挙動は異なる
  • 必ず両プラットフォームでテストする
  • iOSで動いてもAndroidで動くとは限らない(逆も然り)

2. Firebase初期化の制御

Flutterで明示的にFirebaseを初期化する場合:

// Android: google-servicesプラグインをコメントアウト
// plugins {
//   id("com.google.gms.google-services")
// }
<!-- AndroidManifest.xml: 自動初期化を無効化 -->
<meta-data
    android:name="firebase_crashlytics_collection_enabled"
    android:value="false" />

3. エミュレータ接続のベストプラクティス

if (kDebugMode) {
  try {
    await auth.useAuthEmulator('localhost', 9099);
  } catch (e) {
    debugPrint('⚠️ Auth emulator already connected: $e');
  }

  // useFunctionsEmulatorは同期メソッドでエラーをスローしないためtry-catchは不要
  functions.useFunctionsEmulator('localhost', 5001);
}

useAuthEmulatorはホットリロード時に例外をスローする可能性があるためtry-catchで囲む。useFunctionsEmulatorは同期メソッドで例外をスローしないため不要。

まとめ

問題:

  • iOS Simulator + Firebase Emulatorでは動く
  • Android Emulator + リモート環境でも動く
  • Android Emulator + Firebase Emulatorでだけ接続エラー([core/duplicate-app] など)
  • 同じDartコード、同じ初期化ロジックなのに特定の組み合わせでだけ失敗

原因:

  • google-servicesプラグインによるAndroid特有のネイティブ自動初期化
  • Dart側の明示的初期化との競合
  • Emulator環境で顕在化する初期化タイミングの問題

解決:

  1. google-servicesプラグインを無効化
  2. AndroidManifest.xmlで自動初期化を無効化
  3. Dart側でFirebase初期化を完全制御
  4. DIパターンでアーキテクチャも整理

教訓:

  • プラットフォーム固有の挙動を常に意識する
  • 両プラットフォームでの動作確認は必須
  • ネイティブレイヤーの自動初期化に注意

この記事が同じような問題に遭遇した方の助けになれば幸いです!

参考リンク

Discussion