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に接続するときだけ様々なエラーが発生し、正常に動作しませんでした。
原因調査
最初は以下を疑いました:
- ホットリロード時の初期化? →
Firebase.apps.isEmpty
チェック追加でも解決せず - エミュレータのホスト設定? →
localhost
vs10.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")
}
何が問題だったのか:
- Android側(ネイティブレイヤー)でFirebaseAppが自動初期化される(google-servicesプラグイン)
- その後、Dart側でも
Firebase.initializeApp()
を呼び出す - 結果としてFirebaseAppが二重に初期化される
- Dart側でエミュレータ接続設定(
useAuthEmulator
、useFunctionsEmulator
)を行っても、Android側の初期化設定で上書きされてしまい、Firebase Emulatorに接続できない - 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側の初期化を削除してネイティブ自動初期化に完全に任せる」アプローチも検討しました。
試したこと
- google-servicesプラグインを有効化(ネイティブ自動初期化ON)
- 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環境で顕在化する初期化タイミングの問題
解決:
- google-servicesプラグインを無効化
- AndroidManifest.xmlで自動初期化を無効化
- Dart側でFirebase初期化を完全制御
- DIパターンでアーキテクチャも整理
教訓:
- プラットフォーム固有の挙動を常に意識する
- 両プラットフォームでの動作確認は必須
- ネイティブレイヤーの自動初期化に注意
この記事が同じような問題に遭遇した方の助けになれば幸いです!
Discussion