Welcome to Part 8 of the Flutter Interview Questions series! In this installment, we dive deep into Firebase and backend services — one of the most commonly tested areas in Flutter interviews. Whether you are integrating authentication, querying Cloud Firestore, handling push notifications, or setting up crash reporting, this part has you covered. This is part 8 of a 14-part series designed to help you prepare thoroughly for your next Flutter role, so bookmark it and come back as often as you need.
What's in this part?
- Firebase Setup (FlutterFire) & Configuration
- Firebase Authentication (Email, Google, Phone, Multi-provider linking)
- Cloud Firestore (CRUD, real-time streams, pagination, compound queries, transactions, offline persistence, data modeling, security rules)
- Firebase Realtime Database
- Firebase Cloud Messaging (FCM) & Push Notifications
- Firebase Storage (upload, download, security rules)
- Firebase Analytics & Crashlytics
- Firebase Remote Config & Force-Update Mechanism
1.1 Firebase Setup in Flutter (FlutterFire)
Q1: What is FlutterFire and how do you set up Firebase in a Flutter project?
Answer:
FlutterFire is the set of official Flutter plugins that connect your Flutter app to Firebase services. To set up Firebase in a Flutter project:
-
Install the Firebase CLI globally using
npm install -g firebase-toolsor via the standalone binary. -
Install the FlutterFire CLI by running
dart pub global activate flutterfire_cli. -
Log in to Firebase with
firebase login. -
Run
flutterfire configurefrom your Flutter project root. This command automatically:- Creates a Firebase project (or lets you select an existing one).
- Registers your Flutter app for each platform (Android, iOS, Web, macOS).
- Generates a
firebase_options.dartfile containing platform-specific configuration.
-
Add the
firebase_coredependency topubspec.yaml:
dependencies:
firebase_core: ^2.27.0
-
Initialize Firebase in
main.dart:
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
The flutterfire configure command eliminates the need to manually download google-services.json (Android) or GoogleService-Info.plist (iOS), though you can still do manual setup if needed. The generated firebase_options.dart file contains API keys, project IDs, and other configuration for each platform.
Q2: Why is WidgetsFlutterBinding.ensureInitialized() required before Firebase.initializeApp()?
Answer:
WidgetsFlutterBinding.ensureInitialized() ensures the Flutter engine's binding to native platform channels is ready before any plugin communication occurs. Firebase.initializeApp() uses platform channels to communicate with native Firebase SDKs on Android and iOS. If the binding isn't initialized, the platform channel call will fail with an error. This call is only needed when you run asynchronous code before runApp(). If runApp() is called first, it internally calls WidgetsFlutterBinding.ensureInitialized() itself.
Q3: How does firebase_options.dart work and should it be committed to version control?
Answer:
firebase_options.dart is auto-generated by flutterfire configure. It contains a class DefaultFirebaseOptions with a static getter currentPlatform that returns the correct FirebaseOptions (API key, app ID, messaging sender ID, project ID, etc.) based on the current platform using defaultTargetPlatform.
Regarding version control: Yes, it is generally safe to commit. The Firebase API keys in this file are not secret - they are identifiers, not authorization tokens. Firebase security is enforced through Security Rules (Firestore, Realtime Database, Storage) and server-side configurations, not by hiding these keys. However, you should restrict your API keys in the Google Cloud Console to prevent abuse (e.g., limit by app bundle ID, SHA fingerprint, or HTTP referrer).
Q4: How do you handle multiple Firebase projects (e.g., dev, staging, production) in Flutter?
Answer:
There are several approaches:
-
Multiple
firebase_options.dartfiles with flavors: Runflutterfire configurefor each Firebase project and generate separate options files (e.g.,firebase_options_dev.dart,firebase_options_prod.dart). Then select the correct one based on the build flavor:
final options = const String.fromEnvironment('FLAVOR') == 'prod'
? ProdFirebaseOptions.currentPlatform
: DevFirebaseOptions.currentPlatform;
await Firebase.initializeApp(options: options);
-
Use
--dart-define: Pass the environment at build time:
flutter run --dart-define=ENV=dev
- Firebase.initializeApp with named instances: You can initialize secondary Firebase apps:
final secondaryApp = await Firebase.initializeApp(
name: 'SecondaryApp',
options: const FirebaseOptions(...),
);
FirebaseFirestore.instanceFor(app: secondaryApp);
Q5: What are the core FlutterFire plugins and their purposes?
Answer:
The core FlutterFire plugins and their purposes are:
- firebase_core – Initializes Firebase and is required for all other Firebase plugins.
- firebase_auth – Handles user authentication such as email/password, Google, phone, and Apple sign-in.
- cloud_firestore – Provides access to Cloud Firestore, a scalable NoSQL database.
- firebase_database – Offers a Realtime Database for syncing data instantly across clients.
- firebase_storage – Used to store and retrieve files like images, videos, and documents.
- firebase_messaging – Enables push notifications using Firebase Cloud Messaging (FCM).
- firebase_analytics – Tracks user behavior and app events for analytics.
- firebase_crashlytics – Reports and helps analyze app crashes in real time.
- firebase_remote_config – Allows changing app behavior and appearance without updating the app.
- firebase_dynamic_links – Supports deep linking (now deprecated and replaced by App Links).
- firebase_in_app_messaging – Displays targeted in-app messages to engage users.
- firebase_performance – Monitors app performance and network requests.
- firebase_app_check – Protects backend resources by verifying that requests come from authentic apps.
1.2 Firebase Authentication
Q6: How do you implement email/password authentication in Flutter using Firebase?
Answer:
First, enable Email/Password sign-in in the Firebase Console under Authentication > Sign-in method. Then:
Registration:
try {
final credential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
// credential.user contains the User object
await credential.user?.sendEmailVerification(); // optional
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
// Handle weak password
} else if (e.code == 'email-already-in-use') {
// Handle duplicate email
}
}
Sign In:
try {
final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') { ... }
else if (e.code == 'wrong-password') { ... }
}
Auth State Listening:
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
// User is signed out
} else {
// User is signed in
}
});
There are three streams: authStateChanges() (fires on sign-in/out), idTokenChanges() (fires on sign-in/out and token refresh), and userChanges() (fires on all user updates including profile changes).
Q7: How do you implement Google Sign-In with Firebase in Flutter?
Answer:
- Add dependencies:
dependencies:
google_sign_in: ^6.1.0
firebase_auth: ^4.17.0
Configure OAuth consent screen and credentials in Google Cloud Console. For Android, add SHA-1 fingerprint to Firebase project settings.
Implementation:
Future<UserCredential> signInWithGoogle() async {
// Trigger the authentication flow
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
if (googleUser == null) throw Exception('Sign in aborted');
// Obtain the auth details from the request
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// Create a new credential
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// Sign in to Firebase with the Google credential
return await FirebaseAuth.instance.signInWithCredential(credential);
}
For iOS, you must add the GIDClientID to Info.plist and configure the URL scheme. For web, you can use signInWithPopup or signInWithRedirect via FirebaseAuth.instance.signInWithPopup(GoogleAuthProvider()).
Q8: How does phone authentication work in Firebase Flutter?
Answer:
Phone auth sends an SMS verification code. Firebase provides two flows:
Auto-retrieval (Android only): Firebase can automatically detect the incoming SMS and complete verification without user input.
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: '+1234567890',
verificationCompleted: (PhoneAuthCredential credential) async {
// Android only: auto-resolved. Sign in automatically.
await FirebaseAuth.instance.signInWithCredential(credential);
},
verificationFailed: (FirebaseAuthException e) {
// Handle error (invalid number, quota exceeded, etc.)
},
codeSent: (String verificationId, int? resendToken) {
// Store verificationId, show OTP input field
_verificationId = verificationId;
},
codeAutoRetrievalTimeout: (String verificationId) {
// Auto-retrieval timed out, user must enter code manually
},
timeout: const Duration(seconds: 60),
);
Manual verification:
PhoneAuthCredential credential = PhoneAuthProvider.credential(
verificationId: _verificationId,
smsCode: userEnteredCode,
);
await FirebaseAuth.instance.signInWithCredential(credential);
Important notes:
- Phone auth requires SHA-256 fingerprint for Android.
- For testing, Firebase Console allows you to add test phone numbers that work without sending real SMS.
- There are quotas on SMS sends (free tier: 10k/month on Blaze plan for verification SMS).
Q9: How do you handle auth state persistence and sign-out in Flutter?
Answer:
Firebase Auth on mobile (Android/iOS) persists auth state to disk by default. When the app restarts, the user remains signed in. On web, you can configure persistence:
// Web only
await FirebaseAuth.instance.setPersistence(Persistence.LOCAL); // survives browser close
await FirebaseAuth.instance.setPersistence(Persistence.SESSION); // cleared on tab close
await FirebaseAuth.instance.setPersistence(Persistence.NONE); // in-memory only
Check current user on startup:
final user = FirebaseAuth.instance.currentUser;
Sign out:
await FirebaseAuth.instance.signOut();
// If using Google Sign-In, also sign out from Google:
await GoogleSignIn().signOut();
Password reset:
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
Q10: What is Firebase App Check and how does it protect your backend?
Answer:
Firebase App Check helps protect your Firebase backend resources from abuse by ensuring that incoming requests come from your genuine app. It uses attestation providers:
- Android: Play Integrity API (or SafetyNet, deprecated)
- iOS: App Attest or DeviceCheck
- Web: reCAPTCHA v3 or reCAPTCHA Enterprise
Setup:
await FirebaseAppCheck.instance.activate(
androidProvider: AndroidProvider.playIntegrity,
appleProvider: AppleProvider.appAttest,
webProvider: ReCaptchaV3Provider('your-recaptcha-site-key'),
);
Once activated, Firebase services (Firestore, Storage, Functions, etc.) can be configured to require valid App Check tokens, rejecting requests from modified or unofficial app versions. During development, you can use AndroidProvider.debug and AppleProvider.debug debug providers.
Q11: How do you link multiple auth providers to a single Firebase user account?
Answer:
Firebase allows linking multiple authentication methods to one user:
// User is already signed in with email/password
// Now link their Google account
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
final GoogleSignInAuthentication googleAuth = await googleUser!.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
await FirebaseAuth.instance.currentUser?.linkWithCredential(credential);
If the credential is already associated with a different account, a credential-already-in-use error is thrown. You handle this by fetching sign-in methods for the email and guiding the user to sign in with the existing provider first, then link.
To unlink:
await FirebaseAuth.instance.currentUser?.unlink('google.com');
1.3 Cloud Firestore
Q12: How do you perform CRUD operations in Cloud Firestore with Flutter?
Answer:
Create:
// Auto-generated ID
final docRef = await FirebaseFirestore.instance.collection('users').add({
'name': 'Alice',
'age': 30,
'createdAt': FieldValue.serverTimestamp(),
});
// Specific ID
await FirebaseFirestore.instance.collection('users').doc('alice123').set({
'name': 'Alice',
'age': 30,
});
// set with merge (won't overwrite existing fields)
await docRef.set({'email': 'alice@test.com'}, SetOptions(merge: true));
Read:
// Single document
final doc = await FirebaseFirestore.instance.collection('users').doc('alice123').get();
if (doc.exists) {
final data = doc.data() as Map<String, dynamic>;
}
// Entire collection
final snapshot = await FirebaseFirestore.instance.collection('users').get();
for (var doc in snapshot.docs) {
print(doc.data());
}
Update:
await FirebaseFirestore.instance.collection('users').doc('alice123').update({
'age': 31,
'address.city': 'New York', // nested field update using dot notation
'tags': FieldValue.arrayUnion(['flutter']), // add to array
'score': FieldValue.increment(10), // atomic increment
});
Delete:
await FirebaseFirestore.instance.collection('users').doc('alice123').delete();
// Delete a specific field
await docRef.update({'age': FieldValue.delete()});
Q13: What is the difference between get() and snapshots() in Firestore?
Answer:
-
get()is a one-time read (Future). It fetches the data once and returns aDocumentSnapshotorQuerySnapshot. It does not listen for updates.
final snapshot = await FirebaseFirestore.instance.collection('users').get();
-
snapshots()returns a real-time Stream that emits a new snapshot every time the data changes. This is ideal for building reactive UIs.
FirebaseFirestore.instance.collection('users').snapshots().listen((snapshot) {
for (var change in snapshot.docChanges) {
if (change.type == DocumentChangeType.added) { ... }
if (change.type == DocumentChangeType.modified) { ... }
if (change.type == DocumentChangeType.removed) { ... }
}
});
With StreamBuilder:
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('users').snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) return Text('Error');
if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator();
final docs = snapshot.data!.docs;
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index) => Text(docs[index]['name']),
);
},
)
snapshots() also accepts includeMetadataChanges: true to get notified about metadata changes (e.g., pending writes).
Q14: How do you implement pagination in Firestore?
Answer:
Firestore supports cursor-based pagination using startAfter, startAt, endBefore, and endAt combined with limit():
class PaginatedUsers {
DocumentSnapshot? _lastDocument;
final int _pageSize = 20;
bool _hasMore = true;
Future<List<QueryDocumentSnapshot>> fetchNextPage() async {
if (!_hasMore) return [];
Query query = FirebaseFirestore.instance
.collection('users')
.orderBy('createdAt', descending: true)
.limit(_pageSize);
if (_lastDocument != null) {
query = query.startAfterDocument(_lastDocument!);
}
final snapshot = await query.get();
if (snapshot.docs.length < _pageSize) {
_hasMore = false;
}
if (snapshot.docs.isNotEmpty) {
_lastDocument = snapshot.docs.last;
}
return snapshot.docs;
}
}
Key points:
- You must include an
orderByclause that matches the field used in the cursor. -
startAfterDocument()takes the lastDocumentSnapshotfrom the previous page. - Alternatively, use
startAfter([value])with the field value directly. - There is no built-in "offset" pagination in Firestore because it would be inefficient (you'd still read and be charged for skipped documents).
Q15: What are Firestore compound queries and their limitations?
Answer:
Firestore supports combining multiple conditions:
final query = FirebaseFirestore.instance
.collection('products')
.where('category', isEqualTo: 'electronics')
.where('price', isLessThan: 1000)
.where('inStock', isEqualTo: true)
.orderBy('price');
Limitations and rules:
-
Composite indexes required: If a query has a range filter (
<,<=,>,>=) ororderByon a different field than the equality filter, Firestore requires a composite index. Firestore logs an error with a direct link to create the needed index. -
Range filters on single field only (historically): Prior to 2024, range filters (
<,>,!=) could only be applied to one field. With the introduction of Firestore query improvements, multiple inequality filters on different fields are now supported, but they still require a composite index. -
arrayContainslimitations: You can only have onearrayContainsorarrayContainsAnyper query. -
inandnot-in: Limited to 30 values in the list (increased from 10). -
OR queries: Supported via
Filter.or():
FirebaseFirestore.instance.collection('products').where(
Filter.or(
Filter('category', isEqualTo: 'electronics'),
Filter('category', isEqualTo: 'books'),
),
);
- No full-text search: Firestore doesn't support full-text search natively. Use Algolia, Typesense, or Elastic Search integrations.
Q16: What are Firestore transactions and batched writes?
Answer:
Transactions are used when reads and writes must be atomic (e.g., transferring money between accounts):
await FirebaseFirestore.instance.runTransaction((transaction) async {
final senderDoc = await transaction.get(senderRef);
final receiverDoc = await transaction.get(receiverRef);
final senderBalance = senderDoc['balance'] as num;
if (senderBalance < amount) throw Exception('Insufficient funds');
transaction.update(senderRef, {'balance': senderBalance - amount});
transaction.update(receiverRef, {'balance': receiverDoc['balance'] + amount});
});
- Transactions retry up to 5 times on contention.
- All reads must come before writes in a transaction.
- Transactions fail if the document is modified externally during execution.
Batched writes are atomic writes without reads (up to 500 operations):
final batch = FirebaseFirestore.instance.batch();
batch.set(docRef1, {'name': 'Alice'});
batch.update(docRef2, {'score': FieldValue.increment(1)});
batch.delete(docRef3);
await batch.commit(); // atomic: all succeed or all fail
Use transactions when you need to read-then-write. Use batched writes when you just need multiple writes to be atomic.
Q17: How do you use Firestore offline persistence in Flutter?
Answer:
Firestore has offline persistence enabled by default on mobile platforms (Android and iOS). On web, it must be explicitly enabled.
// Enable persistence on web
FirebaseFirestore.instance.settings = const Settings(
persistenceEnabled: true,
cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);
How it works:
- Reads from cache when offline.
- Writes are queued locally and synced when connectivity returns.
-
snapshots()streams will emit cached data withsnapshot.metadata.isFromCache == true. - By default,
get()fetches from server. UseGetOptions(source: Source.cache)to force cache:
final doc = await docRef.get(const GetOptions(source: Source.cache));
Cache size defaults to 100MB. When exceeded, Firestore garbage-collects the least recently used documents. Set CACHE_SIZE_UNLIMITED to disable GC.
Q18: How do you structure data in Firestore - subcollections vs. nested maps vs. root collections?
Answer:
This is a critical Firestore design question:
Subcollections:
users/{userId}/posts/{postId}
- Pros: Each subcollection document is independently queryable, supports pagination, doesn't increase parent document size.
- Cons: Cannot query across all users' posts without Collection Group Queries.
Nested Maps (embedded data):
users/{userId} → { posts: { post1: {...}, post2: {...} } }
- Pros: Single read fetches everything.
- Cons: Document size limit is 1MB. Cannot query individual nested items. Every read fetches all nested data.
Root-level collections with references:
users/{userId}
posts/{postId} → { userId: '...' }
- Pros: Flexible querying, no nesting limits, supports Collection Group Queries.
- Cons: Requires multiple reads (no server-side joins).
Guidelines:
- Use subcollections for unbounded lists (posts, comments, messages).
- Use nested maps for small, bounded data (user preferences, address).
- Use root collections when you frequently query across all documents regardless of parent.
- Use Collection Group Queries to query all subcollections with the same name:
FirebaseFirestore.instance.collectionGroup('posts').where('likes', isGreaterThan: 100).get();
Q19: What are Firestore Security Rules and how do they work?
Answer:
Firestore Security Rules run on Google's servers and determine who can read/write data. They are defined in firestore.rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only read/write their own document
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Anyone can read posts, only authenticated users can create
match /posts/{postId} {
allow read: if true;
allow create: if request.auth != null
&& request.resource.data.title is string
&& request.resource.data.title.size() > 0;
allow update: if request.auth.uid == resource.data.authorId;
allow delete: if request.auth.uid == resource.data.authorId;
}
}
}
Key concepts:
-
request.auth- the authenticated user (null if unauthenticated). -
resource.data- the existing document data. -
request.resource.data- the incoming data (for writes). - Rules are NOT filters - a query must match the rule scope to succeed.
- You can use
get()andexists()to read other documents in rules (limited to 10 per request). - Always deploy rules before going to production. The default test mode rules expire after 30 days.
1.4 Firebase Realtime Database
Q20: What is the difference between Cloud Firestore and Firebase Realtime Database?
Answer:
| Feature | Cloud Firestore | Realtime Database |
|---|---|---|
| Data model | Documents & collections (structured) | Single large JSON tree |
| Querying | Complex queries with compound filters, sorting, pagination | Limited - can only filter/sort on one property at a time |
| Offline support | Yes (mobile default on) | Yes (mobile default on) |
| Scaling | Automatic, scales to millions of concurrent users | Limited - single database in one region, ~200K concurrent |
| Pricing | Per read/write/delete operations + storage | Per bandwidth + storage (no per-operation cost) |
| Real-time | Yes, via snapshots | Yes, historically its primary strength |
| Multi-region | Yes | Single region |
| Transactions | Yes, read-then-write | Yes, but only on single node path at a time |
When to use Realtime Database: High-frequency, low-latency data like presence systems, typing indicators, live cursors, or when bandwidth-based pricing is more cost-effective for your read-heavy use case.
When to use Firestore: Most other cases - richer queries, better scaling, structured data.
Q21: How do you perform CRUD operations in Firebase Realtime Database?
Answer:
final dbRef = FirebaseDatabase.instance.ref();
// Create / Write
await dbRef.child('users/alice').set({
'name': 'Alice',
'age': 30,
});
// Update specific fields (without overwriting entire node)
await dbRef.child('users/alice').update({
'age': 31,
'email': 'alice@example.com',
});
// Read once
final snapshot = await dbRef.child('users/alice').get();
if (snapshot.exists) {
print(snapshot.value);
}
// Real-time listener
dbRef.child('users/alice').onValue.listen((event) {
final data = event.snapshot.value;
print(data);
});
// Delete
await dbRef.child('users/alice').remove();
// Push (auto-generated key - like Firestore's add)
final newPostRef = dbRef.child('posts').push();
await newPostRef.set({'title': 'Hello', 'body': 'World'});
print(newPostRef.key); // unique key like -NxYz123...
Realtime Database queries:
final query = dbRef.child('posts')
.orderByChild('timestamp')
.limitToLast(20);
final snapshot = await query.get();
1.5 Firebase Cloud Messaging (FCM)
Q22: How do you set up Firebase Cloud Messaging for push notifications in Flutter?
Answer:
- Add dependency:
dependencies:
firebase_messaging: ^14.7.0
-
Platform setup:
-
Android: FCM works out of the box after
flutterfire configure. For notification display, add a default notification channel inAndroidManifest.xml. - iOS: Enable Push Notifications capability in Xcode. Upload your APNs key or certificate to Firebase Console. Request permission at runtime.
-
Android: FCM works out of the box after
Request permission (required on iOS, Android 13+):
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
print('Permission status: ${settings.authorizationStatus}');
- Get FCM token:
final token = await FirebaseMessaging.instance.getToken();
print('FCM Token: $token');
// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
// Send to your server
});
- Handle messages:
// Foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Foreground message: ${message.notification?.title}');
// Show local notification using flutter_local_notifications
});
// Background/terminated message tap handler
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
// Navigate to specific screen based on message data
});
// Check if app was opened from a terminated state via notification
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
// Handle navigation
}
Q23: How do you handle background messages in FCM?
Answer:
Background message handling requires a top-level function (not a class method or closure):
// This MUST be a top-level function
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
print('Background message: ${message.messageId}');
// Do NOT update UI here - this runs in an isolate
// You can update local storage, make API calls, etc.
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(MyApp());
}
Key points:
- The
@pragma('vm:entry-point')annotation ensures the function is not tree-shaken in release builds. - The background handler runs in a separate isolate, so it cannot access UI or any state from the main isolate.
- You must call
Firebase.initializeApp()inside the handler since it's a fresh isolate. - For displaying notifications while in foreground, use
flutter_local_notificationspackage since FCM only auto-displays notifications when the app is in background/terminated.
Q24: What is the difference between notification messages and data messages in FCM?
Answer:
Notification messages (display messages):
{
"message": {
"token": "device_token",
"notification": {
"title": "New Message",
"body": "You have a new message"
}
}
}
- Automatically displayed by the system tray when app is in background.
-
onMessagefires in foreground but does NOT auto-display - you must show it manually.
Data messages:
{
"message": {
"token": "device_token",
"data": {
"type": "chat",
"senderId": "123",
"content": "Hello!"
}
}
}
- Never automatically displayed.
- Always delivered to the app's message handler (foreground and background).
- Maximum payload size: 4KB.
- Give you full control over handling.
Combined (most common):
{
"message": {
"token": "device_token",
"notification": { "title": "Chat", "body": "New message" },
"data": { "chatId": "abc123", "senderId": "456" }
}
}
Best practice: Use data-only messages when you need full control over display and handling. Use combined messages for simple notification displays with extra routing data.
Q25: How do you subscribe to FCM topics?
Answer:
Topics allow you to send messages to multiple devices without managing individual tokens:
// Subscribe
await FirebaseMessaging.instance.subscribeToTopic('news');
await FirebaseMessaging.instance.subscribeToTopic('sports');
// Unsubscribe
await FirebaseMessaging.instance.unsubscribeFromTopic('sports');
From the server, you can send to a topic:
{
"message": {
"topic": "news",
"notification": {
"title": "Breaking News",
"body": "Something happened"
}
}
}
You can also use condition-based targeting:
{
"message": {
"condition": "'news' in topics && !('sports' in topics)",
"notification": { ... }
}
}
Topics support up to 5 conditions combined with && and ||. A device can be subscribed to a maximum of 2,000 topics.
1.6 Firebase Storage
Q26: How do you upload and download files using Firebase Storage in Flutter?
Answer:
import 'package:firebase_storage/firebase_storage.dart';
import 'dart:io';
final storage = FirebaseStorage.instance;
// Upload a file
Future<String> uploadFile(File file, String path) async {
final ref = storage.ref().child(path); // e.g., 'images/profile_123.jpg'
// Upload with metadata
final metadata = SettableMetadata(
contentType: 'image/jpeg',
customMetadata: {'uploadedBy': 'user123'},
);
final uploadTask = ref.putFile(file, metadata);
// Monitor upload progress
uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
final progress = snapshot.bytesTransferred / snapshot.totalBytes;
print('Upload progress: ${(progress * 100).toStringAsFixed(1)}%');
});
// Wait for completion
await uploadTask;
// Get download URL
final downloadUrl = await ref.getDownloadURL();
return downloadUrl;
}
// Upload from memory (Uint8List)
Future<void> uploadBytes(Uint8List data) async {
await storage.ref('files/data.bin').putData(data);
}
// Download to local file
Future<void> downloadFile(String remotePath, String localPath) async {
final ref = storage.ref(remotePath);
final file = File(localPath);
await ref.writeToFile(file);
}
// Delete
await storage.ref('images/old.jpg').delete();
// List files in a directory
final result = await storage.ref('images').listAll();
for (var item in result.items) {
print(item.name); // file name
}
for (var prefix in result.prefixes) {
print(prefix.name); // subdirectory name
}
Pause/Resume/Cancel uploads:
final task = ref.putFile(file);
task.pause();
task.resume();
task.cancel();
Q27: How do you set up Firebase Storage security rules?
Answer:
Storage rules are defined in storage.rules:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// User profile images - only owner can write, anyone can read
match /users/{userId}/profile.jpg {
allow read: if true;
allow write: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024 // 5MB max
&& request.resource.contentType.matches('image/.*');
}
// Private documents
match /private/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
Key validation fields:
-
request.resource.size- file size in bytes. -
request.resource.contentType- MIME type. -
request.resource.name- file name. -
resource.size- existing file size (for reads/deletes).
1.7 Firebase Analytics & Crashlytics
Q28: How do you implement Firebase Analytics in Flutter?
Answer:
import 'package:firebase_analytics/firebase_analytics.dart';
final analytics = FirebaseAnalytics.instance;
// Log a custom event
await analytics.logEvent(
name: 'purchase_completed',
parameters: {
'item_id': 'SKU_123',
'item_name': 'Flutter Book',
'price': 29.99,
'currency': 'USD',
},
);
// Log predefined events
await analytics.logLogin(loginMethod: 'google');
await analytics.logSignUp(signUpMethod: 'email');
await analytics.logViewItem(items: [AnalyticsEventItem(itemId: '123', itemName: 'Widget')]);
await analytics.logSearch(searchTerm: 'flutter');
// Set user properties
await analytics.setUserId(id: 'user_123');
await analytics.setUserProperty(name: 'subscription_plan', value: 'premium');
// Screen tracking with Navigator Observer
MaterialApp(
navigatorObservers: [
FirebaseAnalyticsObserver(analytics: analytics),
],
);
// Manual screen tracking
await analytics.logScreenView(
screenName: 'ProductDetailScreen',
screenClass: 'ProductDetailScreen',
);
Best practices:
- Event names max 40 characters, up to 500 distinct event names.
- Up to 25 parameters per event.
- Use predefined events when possible (they appear in standard Firebase reports).
- Events take several hours to appear in the Firebase Console dashboard.
Q29: How do you set up Firebase Crashlytics in Flutter?
Answer:
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Pass all uncaught "fatal" errors from the framework to Crashlytics
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
// Pass all uncaught asynchronous errors that aren't handled by the Flutter framework
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
// Optionally disable in debug mode
// await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(!kDebugMode);
runApp(MyApp());
}
Manual error reporting:
try {
riskyOperation();
} catch (e, stackTrace) {
await FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'riskyOperation failed',
fatal: false,
);
}
Custom keys and logs:
FirebaseCrashlytics.instance.setCustomKey('user_plan', 'premium');
FirebaseCrashlytics.instance.setUserIdentifier('user_123');
FirebaseCrashlytics.instance.log('User tapped checkout button');
Force a test crash:
FirebaseCrashlytics.instance.crash(); // kills the app
Crashlytics groups crashes by root cause, shows stack traces with line numbers (with dSYM/ProGuard mappings), and provides crash-free user statistics.
1.8 Firebase Remote Config
Q30: How do you use Firebase Remote Config in Flutter?
Answer:
Remote Config lets you change app behavior and appearance without publishing an app update.
import 'package:firebase_remote_config/firebase_remote_config.dart';
final remoteConfig = FirebaseRemoteConfig.instance;
// Set defaults (used before first fetch or if fetch fails)
await remoteConfig.setDefaults({
'welcome_message': 'Hello!',
'show_banner': false,
'min_app_version': '1.0.0',
'feature_new_ui': false,
});
// Configure fetch settings
await remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(minutes: 1),
minimumFetchInterval: const Duration(hours: 1), // use Duration.zero for dev
));
// Fetch and activate
await remoteConfig.fetchAndActivate();
// Use values
final welcomeMsg = remoteConfig.getString('welcome_message');
final showBanner = remoteConfig.getBool('show_banner');
final minVersion = remoteConfig.getString('min_app_version');
// Real-time updates (Remote Config v2+)
remoteConfig.onConfigUpdated.listen((event) async {
await remoteConfig.activate();
// Update UI with new values
setState(() {});
});
Use cases:
- Feature flags: Gradually roll out features to a percentage of users.
-
Force update: Check
min_app_versionagainst the current version. - A/B testing: Combined with Firebase A/B Testing to test different values.
- Emergency kill switch: Disable a broken feature remotely.
- Dynamic theming: Change colors, text, and layout without app update.
Important: minimumFetchInterval throttles fetches. In production, use 12 hours to avoid quota limits. In development, use Duration.zero.
Q31: How do you implement a force-update mechanism using Remote Config?
Answer:
Future<void> checkForForceUpdate() async {
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setDefaults({'min_version': '1.0.0', 'update_url': ''});
await remoteConfig.fetchAndActivate();
final minVersion = remoteConfig.getString('min_version');
final updateUrl = remoteConfig.getString('update_url');
final packageInfo = await PackageInfo.fromPlatform(); // from package_info_plus
final currentVersion = packageInfo.version;
if (_isVersionLower(currentVersion, minVersion)) {
// Show non-dismissible dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: Text('Update Required'),
content: Text('Please update to continue using the app.'),
actions: [
TextButton(
onPressed: () => launchUrl(Uri.parse(updateUrl)),
child: Text('Update Now'),
),
],
),
);
}
}
bool _isVersionLower(String current, String minimum) {
final currentParts = current.split('.').map(int.parse).toList();
final minParts = minimum.split('.').map(int.parse).toList();
for (int i = 0; i < 3; i++) {
if (currentParts[i] < minParts[i]) return true;
if (currentParts[i] > minParts[i]) return false;
}
return false;
}
Top comments (0)