Flutter Firebase Integration — Auth, Firestore, and Cloud Services
In this tutorial, you will learn about Flutter Firebase Integration. We cover key concepts, practical examples, and best practices to help you master this topic.
Flutter Firebase integration connects mobile apps to Google's cloud platform, providing authentication, real-time database, file storage, push notifications, and analytics services.
What Will You Learn
- Setting up Firebase in a Flutter project
- Firebase Authentication with email/password and Google sign-in
- Cloud Firestore for real-time NoSQL database
- Firebase Storage for file uploads
- Firebase Cloud Messaging for push notifications
- Firebase Analytics and Crashlytics
Why It Matters
Firebase provides a complete backend solution that scales from Prototype to production. Authentication, database, storage, and analytics are the most common backend needs for mobile apps. Firebase handles server infrastructure, leaving you to focus on the app logic. The Firebase Flutter plugins are first-party, well-maintained, and integrate deeply with Flutter's widget system through streams and FutureBuilder patterns.
Real-World Use
The DodaTech Flutter app uses Firebase Authentication for user login (email and Google providers), Cloud Firestore for course data and user progress, Firebase Storage for profile images and course thumbnails, Firebase Cloud Messaging for push notifications about new courses, and Crashlytics for error reporting.
Learning Path
flowchart LR A[Local Storage] --> B[Firebase Integration\nYou are here] B --> C[Flutter Testing] style B fill:#f90,color:#fff
Firebase Setup
Add Firebase to your Flutter project:
# pubspec.yaml
dependencies:
firebase_core: ^2.25.0
firebase_auth: ^4.17.0
cloud_firestore: ^4.15.0
firebase_storage: ^11.6.0
firebase_messaging: ^14.7.0
firebase_analytics: ^10.8.0
firebase_crashlytics: ^3.4.0
Initialize Firebase in main():
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
Create a Firebase project in the Firebase Console, register your app, and download the google-services.json (Android) or GoogleService-Info.plist (iOS).
Firebase Authentication
Implement email/password authentication:
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Stream<User?> get userStream => _auth.authStateChanges();
User? get currentUser => _auth.currentUser;
Future<UserCredential> signUp(String email, String password) async {
try {
return await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
throw _handleAuthError(e);
}
}
Future<UserCredential> signIn(String email, String password) async {
try {
return await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
throw _handleAuthError(e);
}
}
Future<void> signOut() async {
await _auth.signOut();
}
Future<void> sendPasswordReset(String email) async {
await _auth.sendPasswordResetEmail(email: email);
}
String _handleAuthError(FirebaseAuthException e) {
switch (e.code) {
case 'weak-password': return 'Password is too weak';
case 'email-already-in-use': return 'Account already exists';
case 'user-not-found': return 'No account found with this email';
case 'wrong-password': return 'Incorrect password';
case 'invalid-credential': return 'Invalid email or password';
default: return 'Authentication failed: ${e.message}';
}
}
}
// Usage in a login screen:
class LoginScreen extends StatefulWidget {
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _authService = AuthService();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
String? _error;
Future<void> _login() async {
try {
await _authService.signIn(
_emailController.text.trim(),
_passwordController.text,
);
} on Exception catch (e) {
setState(() => _error = e.toString());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: EdgeInsets.all(24),
child: Column(
children: [
TextField(controller: _emailController, decoration: InputDecoration(labelText: 'Email')),
TextField(controller: _passwordController, decoration: InputDecoration(labelText: 'Password'), obscureText: true),
if (_error != null) Text(_error!, style: TextStyle(color: Colors.red)),
ElevatedButton(onPressed: _login, child: Text('Login')),
],
),
),
);
}
}
Use authStateChanges() to listen to authentication state. The stream emits the current user or null when signed out. Wrap the app with a StreamBuilder that switches between login and home screens.
Cloud Firestore
Firestore is a NoSQL document database with real-time sync:
class FirestoreService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
// Create
Future<void> addCourse(Map<String, dynamic> course) async {
await _db.collection('courses').add(course);
}
// Read (single document)
Future<Map<String, dynamic>?> getCourse(String id) async {
final doc = await _db.collection('courses').doc(id).get();
return doc.data();
}
// Read (realtime stream)
Stream<List<Map<String, dynamic>>> getCoursesStream() {
return _db
.collection('courses')
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => {'id': doc.id, ...doc.data()})
.toList());
}
// Update
Future<void> updateCourse(String id, Map<String, dynamic> data) async {
await _db.collection('courses').doc(id).update(data);
}
// Delete
Future<void> deleteCourse(String id) async {
await _db.collection('courses').doc(id).delete();
}
// Query with filters
Stream<List<Map<String, dynamic>>> getCoursesByCategory(String category) {
return _db
.collection('courses')
.where('category', isEqualTo: category)
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => {'id': doc.id, ...doc.data()})
.toList());
}
// Batch write
Future<void> batchWrite() async {
final batch = _db.batch();
final ref1 = _db.collection('courses').doc();
final ref2 = _db.collection('courses').doc();
batch.set(ref1, {'title': 'Course A'});
batch.set(ref2, {'title': 'Course B'});
await batch.commit();
}
}
Firestore queries are indexed. Create Composite indexes in the Firebase Console for queries with multiple where clauses and orderBy.
Using Firestore in a Widget
StreamBuilder listens to Firestore streams reactively:
class CourseListScreen extends StatelessWidget {
final _firestoreService = FirestoreService();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Courses')),
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: _firestoreService.getCoursesStream(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final courses = snapshot.data!;
if (courses.isEmpty) {
return Center(child: Text('No courses yet'));
}
return ListView.builder(
itemCount: courses.length,
itemBuilder: (_, i) {
final course = courses[i];
return ListTile(
leading: CircleAvatar(child: Text('${i + 1}')),
title: Text(course['title'] as String),
subtitle: Text(course['category'] as String? ?? ''),
trailing: Icon(Icons.chevron_right),
);
},
);
},
),
);
}
}
The StreamBuilder rebuilds whenever Firestore data changes. The stream is automatically cancelled when the widget is disposed.
Firebase Storage
Upload and download files:
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
class StorageService {
final FirebaseStorage _storage = FirebaseStorage.instance;
Future<String> uploadProfileImage(String userId, File imageFile) async {
try {
final ref = _storage.ref().child('profiles/$userId.jpg');
final uploadTask = ref.putFile(imageFile);
// Track progress
uploadTask.snapshotEvents.listen((snapshot) {
final progress = snapshot.bytesTransferred / snapshot.totalBytes * 100;
print('Upload: ${progress.toStringAsFixed(1)}%');
});
final snapshot = await uploadTask;
final downloadUrl = await snapshot.ref.getDownloadURL();
return downloadUrl;
} on FirebaseException catch (e) {
throw Exception('Upload failed: ${e.message}');
}
}
Future<String> uploadCourseImage(String courseId, File imageFile) async {
final ref = _storage.ref().child('courses/$courseId/thumbnail.jpg');
await ref.putFile(imageFile);
return await ref.getDownloadURL();
}
Future<void> deleteImage(String path) async {
final ref = _storage.ref().child(path);
await ref.delete();
}
Future<List<String>> listFiles(String prefix) async {
final ref = _storage.ref().child(prefix);
final result = await ref.listAll();
return result.items.map((item) => item.name).toList();
}
}
Storage paths organize files hierarchically. Use metadata for custom properties. Set security rules in Firebase Console to control read/write access.
Firebase Cloud Messaging (FCM)
Send and receive push notifications:
import 'package:firebase_messaging/firebase_messaging.dart';
class NotificationService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
Future<void> initialize() async {
// Request permission
NotificationSettings settings = await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('Push notifications authorized');
}
// Get FCM token
final token = await _messaging.getToken();
print('FCM Token: $token');
// Listen to foreground messages
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
// Listen to background message taps
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageOpened);
// Handle notification that opened the app from terminated state
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
_handleMessageOpened(initialMessage);
}
}
void _handleForegroundMessage(RemoteMessage message) {
final notification = message.notification;
if (notification != null) {
print('Foreground notification: ${notification.title}');
// Show in-app notification
}
}
void _handleMessageOpened(RemoteMessage message) {
final data = message.data;
print('Notification tapped: $data');
// Navigate to specific screen based on data
}
}
FCM requires Firebase project setup on both Android and iOS. Test notifications from the Firebase Console before implementing custom server logic.
Firebase Analytics and Crashlytics
Track user behavior and monitor crashes:
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observers.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
class AnalyticsService {
final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;
Future<void> initialize() async {
// Enable crashlytics collection
await _crashlytics.setCrashlyticsCollectionEnabled(true);
// Set user identifier
await _crashlytics.setUserIdentifier('user_123');
}
Future<void> logScreenView(String screenName) async {
await _analytics.logScreenView(
screenName: screenName,
screenClass: screenName,
);
}
Future<void> logEvent(String name, {Map<String, dynamic>? parameters}) async {
await _analytics.logEvent(
name: name,
parameters: parameters,
);
}
Future<void> logError(Object error, StackTrace stackTrace) async {
await _crashlytics.recordError(error, stackTrace);
}
Future<void> setUserProperty(String name, String value) async {
await _analytics.setUserProperty(name: name, value: value);
}
// FirebaseAnalyticsObserver for automatic screen tracking
FirebaseAnalyticsObserver get analyticsObserver =>
FirebaseAnalyticsObserver(analytics: _analytics);
}
Crashlytics reports errors automatically. Add FirebaseAnalyticsObserver to your NavigatorObservers for automatic screen tracking.
Common Mistakes
Not handling FirebaseAuth exceptions properly: FirebaseAuthException has specific error codes. Handle each case (weak password, email in use, user not found) with user-friendly messages.
Reading Firestore documents without error handling: Network failures cause exceptions. Use try-catch or handle the error state in StreamBuilder.
Not setting Firestore security rules: By default, Firestore is open in test mode. Set proper rules before production to prevent unauthorized access.
Downloading files without checking cache: Firebase Storage downloads use bandwidth. Cache downloaded files locally to reduce costs and improve offline access.
Not configuring Deep Links for notification navigation: When a user taps a notification, navigate to the correct screen using the data payload. Store navigation routes in notification data.
Practice Questions
- How does
authStateChanges()help manage authentication UI state? - What is the difference between Firestore's
get()andsnapshots()? - How do Firebase Storage security rules differ from Firestore rules?
- What is the purpose of the FCM token?
- Challenge: Build a chat app with Firebase. Use Firebase Auth for authentication, Cloud Firestore for messages (with real-time listener), Firebase Storage for image sharing, and FCM for push notifications. Implement read receipts and typing indicators.
Mini Project
Build a social media feed app:
- Firebase Auth for user registration and login
- Cloud Firestore for posts (image URL, caption, likes, comments)
- Firebase Storage for image uploads
- Real-time feed with StreamBuilder
- Like and comment functionality
- Push notifications for new likes
- Analytics events for post creation and interaction
FAQ
What is Next
Now that you understand Firebase integration, learn about testing in Flutter. Proceed to Flutter Testing for unit tests, widget tests, and integration tests. Then explore Flutter Performance for optimization and profiling.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro