In FlutterFire, how do firebase_options.dart and the native google-services.json and GoogleService-Info.plist files interact when Firebase.initializeApp runs?
answer
- two sources, one default app
- plist configures natively at plugin registration
- Android resources via fromResource
- web has no native file
- soft apiKey comparison
basics
~20 sfirebase_core first adopts any default app the native SDK built from google-services.json or GoogleService-Info.plist, then compares Dart's FirebaseOptions with it: matching options return that app, a different apiKey throws [core/duplicate-app]. Web has no native file, so Dart options are mandatory.
solid answer
~40 s`DefaultFirebaseOptions.currentPlatform` hands `initializeApp` a `FirebaseOptions` for the running platform, but the native side may already have an app: on iOS and macOS, `firebase_core` calls `FirebaseApp.configure()` at plugin registration when a `GoogleService-Info.plist` is bundled, and on Android the `google-services.json` becomes resources that `firebase_core` can read. On its first call `initializeApp` copies native apps into the Dart registry. With no default app it creates one from your options — or, on Android with no options, from those resources — and otherwise throws `[core/not-initialized]`. With a default app already there it soft-compares `apiKey` (and a passed `databaseURL` or `storageBucket`): a match returns the existing app, a mismatch throws `[core/duplicate-app]`. Web has no native file, so options are required.
code
dart · 17 linesimport 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'firebase_options.dart';
Future<FirebaseApp> initFirebase() async {
try {
return await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
} on FirebaseException catch (e) {
if (e.code == 'duplicate-app') {
debugPrint('Native config file and firebase_options.dart disagree: $e');
}
rethrow;
}
}go deeper
Know that there are two configuration sources, the Dart options file and the native files, and that web relies only on the Dart options.
Walk through initializeApp's decisions: adopt native apps, create from options, read Android resources, compare on apiKey, throw not-initialized or duplicate-app.
Diagnose a duplicate-app crash as a native file from another project, and decide whether a product you use still needs the native files.
Choose one source of truth for Firebase configuration across platforms and make the build enforce it, so native files and Dart options cannot drift apart.
## Two sources of the same configuration A FlutterFire app can learn its Firebase configuration from two places: - **The Dart options** in `lib/firebase_options.dart`, generated by `flutterfire configure`. `DefaultFirebaseOptions.currentPlatform` returns a `const FirebaseOptions` for the running platform; its constructor requires `apiKey`, `appId`, `messagingSenderId` and `projectId`, and carries optional fields such as `storageBucket`, `authDomain` and `iosBundleId`. - **The native config files**: `google-services.json` in the Android app module and `GoogleService-Info.plist` in the Apple app bundle. These are what the native Firebase SDKs read when a pure Android or iOS app starts Firebase. Understanding `Firebase.initializeApp` means knowing how `firebase_core` reconciles the two. ## What happens natively before Dart runs - **iOS and macOS.** When `firebase_core`'s Swift plugin is registered, it checks whether default Firebase options exist in the bundle — that is, whether a `GoogleService-Info.plist` was bundled — and if so, and no default app exists yet, it calls `FirebaseApp.configure()`. A native default app can therefore exist before your `main` has awaited anything. - **Android.** The Google Services Gradle plugin turns `google-services.json` into Android resources at build time. `firebase_core` can read them on request through `FirebaseOptions.fromResource`, and its core initialization reports every Firebase app that already exists natively. - **Web.** There is no native file. The Firebase JavaScript SDK is configured only with the options Dart passes in. ## How `initializeApp` resolves the default app On the first call, `firebase_core` runs its core initialization, which copies any natively created apps into the Dart registry. For the default app (no `name`, or `[DEFAULT]`) the pinned method-channel implementation then decides: 1. **No default app, options passed** — creates `[DEFAULT]` from the Dart options. 2. **No default app, no options, on Android** — reads options from the resources built from `google-services.json`, then creates the app; if those resources are missing, the load fails with an error telling you to check them. 3. **No default app, no options, anywhere else** — throws `[core/not-initialized]`, "Firebase has not been correctly initialized." 4. **Default app exists, no options** — returns it. 5. **Default app exists, options passed** — performs a *soft* comparison: if `apiKey` differs, or a `databaseURL` or `storageBucket` you passed differs, it throws `[core/duplicate-app]` ("A Firebase App named \"[DEFAULT]\" already exists"); otherwise it returns the existing app. The web implementation mirrors this with one difference: with no default app it asserts that options are non-null, because it has nowhere else to look. | Platform | Native file read | Required to pass Dart options? | |---|---|---| | Android | `google-services.json`, via generated resources | No, if the file is present; recommended anyway | | iOS / macOS | `GoogleService-Info.plist`, at plugin registration | No, if the plist is bundled; recommended anyway | | Web | none | **Yes** | | Windows / Linux | none | Yes, for whatever the options file configures | ## Why the recommended path passes options everywhere The setup guide's `Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)` makes Dart the single, visible source of truth: the same line works on every platform, it is checked in with the code, and it removes the "which file did the build pick up?" question. The native files do not disappear, though: - The `initializeApp` API documentation notes that a bundled `google-services.json` or `GoogleService-Info.plist` still creates a native `[DEFAULT]` app — and **you must still call `initializeApp`** before using any plugin. - Some products read native configuration themselves; the FlutterFire docs, for example, tell you to add both native files for GitHub sign-in on mobile. ## Diagnosing which configuration won - Log `Firebase.app().options.projectId` and `apiKey` right after initialization; that is the app every plugin will use. - On iOS, check which `GoogleService-Info.plist` is actually in the built bundle, not just in the repository. - On Android, check which `google-services.json` the build picked up for the variant you ran. - Compare both with `firebase_options.dart`; any project mismatch is a bug even when it does not crash yet. ## The consequence worth remembering Because a bundled native file creates a default app *first*, the Dart options are **compared against it, not substituted for it**. If both describe the same Firebase app, the comparison passes and nothing is visible. If they describe different projects — typically a stale plist left in the bundle after switching projects — startup fails with `[core/duplicate-app]`. Keeping the two in step, or deliberately relying on only one, is part of wiring Firebase correctly.
- If you pass DefaultFirebaseOptions.currentPlatform everywhere, can you delete the native config files?Core initialization works without them, because the Dart options create the default app. Some products still read native configuration, though — the FlutterFire docs require both native files for GitHub sign-in on mobile — so delete them only after checking every product you use, and never leave a stale one bundled.
- Why does a hot restart not trip duplicate-app, even though the native default app survives it?After the restart the Dart registry is empty, so `initializeApp` re-runs core initialization, adopts the surviving native app, and soft-compares your options with it. The options are the same as before the restart, so the comparison passes and the existing app is returned.
saying these in an interview costs you the question
- Dart options always override whatever the native config files say.
- FlutterFire ignores GoogleService-Info.plist entirely once firebase_options.dart exists.
- On web, firebase_core reads google-services.json from the web folder.
- A bundled native config file means Firebase.initializeApp need not be called.
- Any difference at all between native and Dart options throws duplicate-app.