1. Pengenalan Flutter + Firebase
Flutter adalah framework cross-platform dari Google untuk membangun aplikasi mobile, web, dan desktop dari satu codebase. Firebase adalah platform Backend-as-a-Service (BaaS) dari Google yang menyediakan berbagai layanan siap pakai untuk membangun aplikasi modern tanpa harus membuat backend dari nol.
Ketika digabungkan, Flutter dan Firebase membentuk kombinasi yang sangat powerful β Anda bisa membangun aplikasi mobile dengan UI yang indah dan performa tinggi, dengan backend yang scalable tanpa harus mengelola server sendiri.
Mengapa Flutter + Firebase?
| Keunggulan | Penjelasan |
|---|---|
| Single Codebase | Satu kode untuk iOS, Android, Web β hemat waktu development |
| Zero Backend Setup | Firebase menyediakan auth, database, storage, push notification tanpa server |
| Real-time | Firestore mendukung real-time sync otomatis ke semua device |
| Scalable | Firebase Auto Scaling menangani jutaan user tanpa konfigurasi |
| Google Ecosystem | Integrasi mendalam dengan Google Cloud, Analytics, Crashlytics |
| Plugin Resmi | Firebase memiliki plugin resmi FlutterFire yang maintained oleh Google |
Layanan Firebase untuk Flutter
| Layanan | Fungsi | Kasus Penggunaan |
|---|---|---|
| Authentication | Manajemen user & login | Login email, Google, Apple, phone |
| Cloud Firestore | NoSQL database real-time | Data app, chat, feed |
| Firebase Storage | File & media storage | Upload foto, video, dokumen |
| Cloud Messaging | Push notification | Notifikasi ke user |
| Cloud Functions | Serverless backend | Logic server-side, scheduled tasks |
| Analytics | Analytics & Crashlytics | Monitoring performa & error |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FLUTTER APP β
β ββββββββββββ ββββββββββββ ββββββββββββββββββββββββ β
β β UI β β State β β Firebase Services β β
β β Layer β β Mgmt β β β β
β β(Widgets) β β(Provider/β β ββββββββ ββββββββββ β β
β β β β Riverpod)β β β Auth β βFirestoreβ β β
β ββββββββββββ ββββββββββββ β ββββ¬ββββ βββββ¬βββββ β β
β β β β β β
β β ββββ΄ββββ βββββ΄βββββ β β
β β βStoragβ β FCM β β β
β β ββββββββ ββββββββββ β β
β ββββββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββ
β
βββββββββββββββββββββββββ
β FIREBASE ββ
β βββββββββββββββββ ββ
β β Authenticationβ ββ
β βββββββββββββββββ ββ
β βββββββββββββββββ ββ
β β Firestore ββββββ
β βββββββββββββββββ β
β βββββββββββββββββ β
β β Storage ββββββ
β βββββββββββββββββ
β βββββββββββββββββ
β β Cloud Messagingβ
β βββββββββββββββββ
βββββββββββββββββββββββ
2. Setup Firebase di Flutter
Sebelum menggunakan layanan Firebase, Anda perlu melakukan setup di kedua sisi: Firebase Console dan project Flutter.
Langkah 1: Buat Project Firebase
- Buka Firebase Console
- Klik "Add project" dan masukkan nama project
- Enable/disable Google Analytics sesuai kebutuhan
- Klik "Create project"
Langkah 2: Install FlutterFire CLI
# Install FlutterFire CLI secara global dart pub global activate flutterfire_cli # Pastikan PATH sudah benek export PATH="$PATH:$HOME/.pub-cache/bin" # Jalankan konfigurasi Firebase dari root project Flutter cd my_flutter_app flutterfire configure
Perintah flutterfire configure akan:
- Menghubungkan project Flutter dengan project Firebase yang sudah ada
- Mendownload file
google-services.json(Android) danGoogleService-Info.plist(iOS) - Membuat file
firebase_options.dartdengan konfigurasi otomatis
Langkah 3: Tambahkan Dependencies
dependencies:
flutter:
sdk: flutter
# Firebase Core β wajib ada
firebase_core: ^3.8.1
# Firebase Auth
firebase_auth: ^5.4.1
# Cloud Firestore
cloud_firestore: ^5.6.5
# Firebase Storage
firebase_storage: ^12.4.1
# Cloud Messaging
firebase_messaging: ^15.2.1
# Cloud Functions (jika perlu panggil fungsi backend)
cloud_functions: ^5.2.1
# Firebase Analytics & Crashlytics
firebase_analytics: ^11.4.1
firebase_crashlytics: ^4.3.1
# State Management
provider: ^6.1.2
# atau riverpod
# flutter_riverpod: ^2.6.1
Langkah 4: Inisialisasi Firebase di main()
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';
void main() async {
// Pastikan widget binding sudah terinisialisasi
WidgetsFlutterBinding.ensureInitialized();
// Inisialisasi Firebase
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Firebase App',
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
),
home: const AuthGate(),
);
}
}
// Gate: cek status login, arahkan ke halaman yang tepat
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
// Loading
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
// Sudah login β ke Home
if (snapshot.hasData) {
return const HomePage();
}
// Belum login β ke Login
return const LoginPage();
},
);
}
}
Selalu panggil Firebase.initializeApp() sebelum runApp(). Jika tidak, Anda akan mendapatkan error FirebaseException: [core/not-initialized]. Gunakan WidgetsFlutterBinding.ensureInitialized() sebelum inisialisasi.
3. Firebase Authentication
Firebase Authentication menyediakan solusi lengkap untuk mengelola autentikasi user β mulai dari email/password, social login (Google, Apple, Facebook), hingga phone number verification.
3.1 Login dengan Email & Password
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
// Stream perubahan status autentikasi
Stream<User?> get authStateChanges => _auth.authStateChanges();
// User yang sedang login
User? get currentUser => _auth.currentUser;
// Register dengan email & password
Future<UserCredential?> registerWithEmail({
required String email,
required String password,
required String displayName,
}) async {
try {
final credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// Update display name
await credential.user?.updateDisplayName(displayName);
return credential;
} on FirebaseAuthException catch (e) {
_handleAuthError(e);
return null;
}
}
// Login dengan email & password
Future<UserCredential?> loginWithEmail({
required String email,
required String password,
}) async {
try {
return await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
_handleAuthError(e);
return null;
}
}
// Reset password
Future<void> resetPassword(String email) async {
await _auth.sendPasswordResetEmail(email: email);
}
// Logout
Future<void> signOut() async {
await _auth.signOut();
}
// Handle error autentikasi
void _handleAuthError(FirebaseAuthException e) {
switch (e.code) {
case 'user-not-found':
print('User tidak ditemukan');
break;
case 'wrong-password':
print('Password salah');
break;
case 'email-already-in-use':
print('Email sudah terdaftar');
break;
case 'weak-password':
print('Password terlalu lemah');
break;
case 'invalid-email':
print('Format email tidak valid');
break;
default:
print('Error: ${e.message}');
}
}
}
3.2 Login dengan Google
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
class GoogleAuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _google = GoogleSignIn();
// Login dengan Google
Future<UserCredential?> signInWithGoogle() async {
try {
// 1. Munculkan dialog Google Sign-In
final GoogleSignInAccount? googleUser = await _google.signIn();
if (googleUser == null) return null; // User membatalkan
// 2. Ambil authentication details
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
// 3. Buat credential Firebase
final OAuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// 4. Login ke Firebase dengan credential
return await _auth.signInWithCredential(credential);
} catch (e) {
print('Error Google Sign-In: $e');
return null;
}
}
// Logout dari Google dan Firebase
Future<void> signOutGoogle() async {
await _google.signOut();
await _auth.signOut();
}
}
3.3 Login dengan Phone Number
class PhoneAuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
ConfirmationResult? _confirmationResult;
// Kirim OTP ke nomor telepon
Future<void> sendOTP(String phoneNumber) async {
_confirmationResult = await _auth.signInWithPhoneNumber(
phoneNumber,
// Opsional: verifikasi otomatis tanpa SMS (reCAPTCHA)
// PhoneAuthProvider.credential(
// verificationCompleted: (PhoneAuthCredential credential) async {
// await _auth.signInWithCredential(credential);
// },
// ),
);
}
// Verifikasi OTP yang dimasukkan user
Future<UserCredential?> verifyOTP(String otp) async {
if (_confirmationResult == null) return null;
try {
return await _confirmationResult!.confirm(otp);
} catch (e) {
print('OTP salah: $e');
return null;
}
}
}
3.4 UI Login Sederhana
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _emailCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
final _authService = AuthService();
bool _isLoading = false;
Future<void> _handleLogin() async {
setState(() => _isLoading = true);
final result = await _authService.loginWithEmail(
email: _emailCtrl.text.trim(),
password: _passwordCtrl.text,
);
setState(() => _isLoading = false);
if (result != null && mounted) {
// AuthGate akan otomatis navigasi ke HomePage
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Login berhasil!')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.lock_outline, size: 80, color: Colors.blue),
const SizedBox(height: 24),
const Text('Masuk ke Akun Anda',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
// Email Field
TextField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// Password Field
TextField(
controller: _passwordCtrl,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock_outlined),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Login Button
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(
onPressed: _isLoading ? null : _handleLogin,
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Masuk'),
),
),
const SizedBox(height: 16),
// Google Sign-In
OutlinedButton.icon(
onPressed: () => GoogleAuthService().signInWithGoogle(),
icon: const Icon(Icons.g_mobiledata, size: 24),
label: const Text('Masuk dengan Google'),
),
],
),
),
),
);
}
}
4. Cloud Firestore
Cloud Firestore adalah NoSQL database dari Firebase yang mendukung real-time data sync, offline support, dan scalable queries. Data disimpan dalam koleksi yang berisi dokumen, mirip seperti tabel dan baris di SQL tetapi lebih fleksibel.
βββββββββββββββββββββββββββββββββββββββββββββββ
β FIRESTORE DATABASE β
β β
β Collection: "users" β
β βββββββββββββββββββββββββββββββββββββββ β
β β Doc: user123 β β
β β { β β
β β name: "Budi", β β
β β email: "budi@mail.com", β β
β β age: 25, β β
β β createdAt: Timestamp, β β
β β subcollection: "posts" β β β
β β } β β
β βββββββββββββββββββββββββββββββββββββββ β
β β
β Collection: "posts" β
β βββββββββββββββββββββββββββββββββββββββ β
β β Doc: post456 β β
β β { β β
β β title: "Hello World", β β
β β content: "First post...", β β
β β authorId: "user123", β β
β β likes: [user123, user789], β β
β β createdAt: Timestamp, β β
β β } β β
β βββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββ
4.1 Model Data
import 'package:cloud_firestore/cloud_firestore.dart';
class UserModel {
final String uid;
final String name;
final String email;
final int age;
final DateTime createdAt;
final String? avatarUrl;
UserModel({
required this.uid,
required this.name,
required this.email,
required this.age,
required this.createdAt,
this.avatarUrl,
});
// Convert dari Firestore Document
factory UserModel.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return UserModel(
uid: doc.id,
name: data['name'] ?? '',
email: data['email'] ?? '',
age: data['age'] ?? 0,
createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
avatarUrl: data['avatarUrl'],
);
}
// Convert ke Map untuk disimpan ke Firestore
Map<String, dynamic> toFirestore() {
return {
'name': name,
'email': email,
'age': age,
'createdAt': FieldValue.serverTimestamp(),
'avatarUrl': avatarUrl,
};
}
}
4.2 CRUD Operations
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/user_model.dart';
class FirestoreService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
// βββββββββββββ CREATE βββββββββββββ
Future<void> createUser(UserModel user) async {
await _db.collection('users').doc(user.uid).set(user.toFirestore());
}
// βββββββββββββ READ (Single) βββββββββββββ
Future<UserModel?> getUser(String uid) async {
final doc = await _db.collection('users').doc(uid).get();
if (doc.exists) {
return UserModel.fromFirestore(doc);
}
return null;
}
// βββββββββββββ READ (Real-time Stream) βββββββββββββ
Stream<UserModel?> userStream(String uid) {
return _db.collection('users').doc(uid).snapshots().map(
(doc) => doc.exists ? UserModel.fromFirestore(doc) : null,
);
}
// βββββββββββββ READ (List) βββββββββββββ
Future<List<UserModel>> getUsers({int limit = 20}) async {
final snapshot = await _db
.collection('users')
.orderBy('createdAt', descending: true)
.limit(limit)
.get();
return snapshot.docs
.map((doc) => UserModel.fromFirestore(doc))
.toList();
}
// βββββββββββββ READ (Query) βββββββββββββ
Future<List<UserModel>> searchUsers(String query) async {
final snapshot = await _db
.collection('users')
.where('name', isGreaterThanOrEqualTo: query)
.where('name', isLessThanOrEqualTo: '$query\uf8ff')
.get();
return snapshot.docs
.map((doc) => UserModel.fromFirestore(doc))
.toList();
}
// βββββββββββββ UPDATE βββββββββββββ
Future<void> updateUser(String uid, Map<String, dynamic> data) async {
await _db.collection('users').doc(uid).update(data);
}
// Update dengan increment
Future<void> incrementField(String uid, String field, int amount) async {
await _db.collection('users').doc(uid).update({
field: FieldValue.increment(amount),
});
}
// βββββββββββββ DELETE βββββββββββββ
Future<void> deleteUser(String uid) async {
await _db.collection('users').doc(uid).delete();
}
// βββββββββββββ BATCH OPERATIONS βββββββββββββ
Future<void> batchDelete(List<String> uids) async {
final batch = _db.batch();
for (final uid in uids) {
final ref = _db.collection('users').doc(uid);
batch.delete(ref);
}
await batch.commit();
}
// βββββββββββββ TRANSACTIONS βββββββββββββ
Future<void> transferPoints(String fromId, String toId, int amount) async {
await _db.runTransaction((transaction) async {
final fromDoc = await transaction.get(_db.collection('users').doc(fromId));
final toDoc = await transaction.get(_db.collection('users').doc(toId));
if (!fromDoc.exists || !toDoc.exists) {
throw Exception('User tidak ditemukan');
}
final fromData = fromDoc.data()!;
final toData = toDoc.data()!;
if ((fromData['points'] ?? 0) < amount) {
throw Exception('Poin tidak cukup');
}
transaction.update(_db.collection('users').doc(fromId), {
'points': FieldValue.increment(-amount),
});
transaction.update(_db.collection('users').doc(toId), {
'points': FieldValue.increment(amount),
});
});
}
}
4.3 Real-time Listener di Widget
class UserListPage extends StatelessWidget {
const UserListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Semua Users')),
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('users')
.orderBy('createdAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
// Loading
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// Error
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
// Kosong
final docs = snapshot.data?.docs ?? [];
if (docs.isEmpty) {
return const Center(child: Text('Belum ada user'));
}
// Tampilkan list
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index) {
final user = UserModel.fromFirestore(docs[index]);
return ListTile(
leading: CircleAvatar(
child: Text(user.name[0].toUpperCase()),
),
title: Text(user.name),
subtitle: Text(user.email),
trailing: Text('${user.age} th'),
);
},
);
},
),
);
}
}
Firestore menyediakan offline support secara otomatis di mobile. Untuk mengaktifkannya secara eksplisit: FirebaseFirestore.instance.settings = const Settings(persistenceEnabled: true);. Data akan di-cache local dan disinkronkan otomatis saat online.
5. Firebase Storage
Firebase Storage menyediakan layanan upload dan download file berbasis Google Cloud Storage. Cocok untuk menyimpan foto profil, gambar post, video, dokumen, dan file lainnya.
5.1 Upload File
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:image_picker/image_picker.dart';
class StorageService {
final FirebaseStorage _storage = FirebaseStorage.instance;
// Upload foto profil
Future<String?> uploadProfilePhoto({
required String uid,
required File imageFile,
}) async {
try {
// Buat path unik
final ref = _storage.ref().child('profiles/$uid/avatar.jpg');
// Upload file
final uploadTask = await ref.putFile(
imageFile,
SettableMetadata(
contentType: 'image/jpeg',
customMetadata: {'uploadedBy': uid},
),
);
// Ambil URL download
final downloadUrl = await uploadTask.ref.getDownloadURL();
return downloadUrl;
} catch (e) {
print('Upload error: $e');
return null;
}
}
// Upload dengan progress tracking
Stream<TaskSnapshot> uploadWithProgress({
required String path,
required File file,
required String contentType,
}) {
final ref = _storage.ref().child(path);
final uploadTask = ref.putFile(
file,
SettableMetadata(contentType: contentType),
);
return uploadTask.snapshotEvents;
}
// Upload gambar dari gallery
Future<String?> uploadImageFromGallery({
required String uid,
required String postId,
}) async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1080,
maxHeight: 1080,
imageQuality: 85,
);
if (pickedFile == null) return null;
final file = File(pickedFile.path);
final ext = pickedFile.path.split('.').last;
final path = 'posts/$uid/${DateTime.now().millisecondsSinceEpoch}.$ext';
final ref = _storage.ref().child(path);
final uploadTask = await ref.putFile(file);
return await uploadTask.ref.getDownloadURL();
}
// Download file
Future<File?> downloadFile({
required String url,
required String savePath,
}) async {
try {
final ref = _storage.refFromURL(url);
final file = File(savePath);
await ref.writeToFile(file);
return file;
} catch (e) {
print('Download error: $e');
return null;
}
}
// Hapus file
Future<void> deleteFile(String url) async {
try {
final ref = _storage.refFromURL(url);
await ref.delete();
} catch (e) {
print('Delete error: $e');
}
}
}
5.2 Upload Widget dengan Progress
class UploadImageWidget extends StatefulWidget {
final String uid;
const UploadImageWidget({super.key, required this.uid});
@override
State<UploadImageWidget> createState() => _UploadImageWidgetState();
}
class _UploadImageWidgetState extends State<UploadImageWidget> {
final _storageService = StorageService();
double _progress = 0;
String? _downloadUrl;
Future<void> _pickAndUpload() async {
final picker = ImagePicker();
final file = await picker.pickImage(source: ImageSource.gallery);
if (file == null) return;
final stream = _storageService.uploadWithProgress(
path: 'profiles/${widget.uid}/photo.jpg',
file: File(file.path),
contentType: 'image/jpeg',
);
stream.listen(
(TaskSnapshot snapshot) {
setState(() {
_progress = snapshot.bytesTransferred / snapshot.totalBytes;
});
},
onDone: () async {
final url = await snapshot.ref.getDownloadURL();
setState(() {
_downloadUrl = url;
_progress = 1.0;
});
},
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Preview gambar
if (_downloadUrl != null)
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(_downloadUrl!),
)
else
const CircleAvatar(
radius: 50,
child: Icon(Icons.person, size: 50),
),
const SizedBox(height: 8),
// Progress bar
if (_progress > 0 && _progress < 1)
LinearProgressIndicator(value: _progress),
const SizedBox(height: 8),
FilledButton.icon(
onPressed: _pickAndUpload,
icon: const Icon(Icons.camera_alt),
label: Text(_downloadUrl == null ? 'Pilih Foto' : 'Ganti Foto'),
),
],
);
}
}
6. Cloud Messaging (FCM)
Firebase Cloud Messaging (FCM) adalah solusi cross-platform untuk mengirim push notification ke Android, iOS, dan web. FCM gratis, scalable, dan terintegrasi langsung dengan Firebase.
6.1 Setup FCM di Flutter
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_auth/firebase_auth.dart';
class MessagingService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
// Inisialisasi messaging
Future<void> initialize() async {
// 1. Minta permission (iOS & web)
final settings = await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
criticalAlert: true,
);
print('Permission status: ${settings.authorizationStatus}');
// 2. Ambil FCM token
final token = await _messaging.getToken();
print('FCM Token: $token');
// 3. Simpan token ke Firestore (untuk targeting)
if (token != null) {
await _saveTokenToFirestore(token);
}
// 4. Listen token refresh
_messaging.onTokenRefresh.listen(_saveTokenToFirestore);
// 5. Handle message saat app terminated
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
// 6. Handle message foreground
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
// 7. Handle background message (harus di top-level function)
FirebaseMessaging.onBackgroundMessage(_handleBackgroundMessage);
}
// Simpan token ke Firestore
Future<void> _saveTokenToFirestore(String token) async {
final uid = FirebaseAuth.instance.currentUser?.uid;
if (uid == null) return;
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'fcmTokens': FieldValue.arrayUnion([token]),
});
}
// Handle message di foreground
void _handleForegroundMessage(RemoteMessage message) {
print('Foreground message: ${message.notification?.title}');
// Tampilkan custom in-app notification
// Bisa pakai flutter_local_notifications atau custom widget
if (message.notification != null) {
_showInAppNotification(
title: message.notification!.title ?? '',
body: message.notification!.body ?? '',
data: message.data,
);
}
}
// Handle message saat app dibuka dari notifikasi
void _handleMessage(RemoteMessage message) {
final route = message.data['route'];
if (route != null) {
// Navigasi ke halaman tertentu
// navigatorKey.currentState?.pushNamed(route);
}
}
// Top-level function untuk background message
static Future<void> _handleBackgroundMessage(
RemoteMessage message
) async {
// Pastikan Firebase sudah terinisialisasi
await Firebase.initializeApp();
print('Background message: ${message.notification?.title}');
}
void _showInAppNotification({
required String title,
required String body,
required Map<String, dynamic> data,
}) {
// Implementasi in-app notification (banner/toast)
print('In-App: $title - $body');
}
}
6.2 Background Handler (Top-Level)
// background_handler.dart
// HARUS di top-level function, bukan di dalam class atau method lain
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_core/firebase_core.dart';
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(
RemoteMessage message
) async {
// Inisialisasi Firebase untuk background isolate
await Firebase.initializeApp();
print('=== BACKGROUND MESSAGE ===');
print('Title: ${message.notification?.title}');
print('Body: ${message.notification?.body}');
print('Data: ${message.data}');
// Simpan message ke local storage atau lakukan sync
// Gunakan WorkManager atau scheduled task untuk processing
}
// Di main.dart, register handler SEBELUM runApp():
// void main() async {
// WidgetsFlutterBinding.ensureInitialized();
// await Firebase.initializeApp(...);
// FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
// runApp(MyApp());
// }
6.3 Kirim Notifikasi via Cloud Functions
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
// Trigger: saat dokumen baru ditambahkan ke collection "notifications"
exports.sendNewPostNotification = functions.firestore
.document("posts/{postId}")
.onCreate(async (snap, context) => {
const postData = snap.data();
const authorId = postData.authorId;
// Ambil semua kecuali author
const usersSnapshot = await admin.firestore()
.collection("users")
.where("__name__", "!=", authorId)
.get();
const tokens = [];
usersSnapshot.forEach((doc) => {
const fcmTokens = doc.data().fcmTokens || [];
tokens.push(...fcmTokens);
});
if (tokens.length === 0) return null;
// Kirim notifikasi via FCM
const message = {
notification: {
title: "Post Baru! π",
body: `${postData.authorName} baru saja memposting: ${postData.title}`,
},
data: {
route: "/post/${context.params.postId}",
postId: context.params.postId,
},
tokens: tokens,
};
// Kirim ke semua token (batch)
const response = await admin.messaging().sendEachForMulticast(message);
// Bersihkan token yang sudah tidak valid
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(tokens[idx]);
}
});
// Hapus token invalid dari Firestore
if (failedTokens.length > 0) {
const batch = admin.firestore().batch();
usersSnapshot.forEach((doc) => {
const userTokens = doc.data().fcmTokens || [];
const validTokens = userTokens
.filter((t) => !failedTokens.includes(t));
batch.update(doc.ref, { fcmTokens: validTokens });
});
await batch.commit();
}
return response;
});
// Trigger: saat ada komentar baru
exports.sendCommentNotification = functions.firestore
.document("posts/{postId}/comments/{commentId}")
.onCreate(async (snap, context) => {
const commentData = snap.data();
const postId = context.params.postId;
// Ambil data post
const postDoc = await admin.firestore()
.collection("posts").doc(postId).get();
const postData = postDoc.data();
// Kirim ke pemilik post
if (postData.authorId !== commentData.authorId) {
const ownerDoc = await admin.firestore()
.collection("users").doc(postData.authorId).get();
const tokens = ownerDoc.data().fcmTokens || [];
if (tokens.length > 0) {
await admin.messaging().sendEachForMulticast({
notification: {
title: "Komentar Baru π¬",
body: `${commentData.authorName}: ${commentData.text}`,
},
data: { route: "/post/${postId}" },
tokens: tokens,
});
}
}
});
7. Cloud Functions
Cloud Functions memungkinkan Anda menjalankan backend logic tanpa mengelola server. Function akan otomatis dipanggil berdasarkan trigger tertentu (Firestore event, HTTP request, scheduled time, dll).
7.1 Panggil Cloud Function dari Flutter
import 'package:cloud_functions/cloud_functions.dart';
class CloudFunctionService {
// Panggil callable function
Future<dynamic> callFunction({
required String name,
Map<String, dynamic>? params,
}) async {
try {
final result = await FirebaseFunctions.instance
.httpsCallable(name)
.call(params ?? {});
return result.data;
} on FirebaseFunctionsException catch (e) {
print('Cloud Function error: ${e.code} - ${e.message}');
return null;
}
}
// Contoh: generate custom token untuk admin
Future<String?> getAdminToken(String uid) async {
final result = await callFunction(
name: 'getAdminToken',
params: {'uid': uid},
);
return result?['token'];
}
// Contoh: proses data di server
Future<Map<String, dynamic>?> processPayment({
required String userId,
required double amount,
required String method,
}) async {
final result = await callFunction(
name: 'processPayment',
params: {
'userId': userId,
'amount': amount,
'method': method,
},
);
return Map<String, dynamic>.from(result ?? {});
}
}
7.2 Cloud Function Callable
// functions/index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
// Callable function: buat custom claim
exports.setAdminClaim = functions.https.onCall(async (data, context) => {
// Cek apakah caller adalah admin
if (!context.auth) {
throw new functions.https.HttpsError(
"unauthenticated", "Harus login");
}
const callerDoc = await admin.firestore()
.collection("users").doc(context.auth.uid).get();
if (!callerDoc.data().isAdmin) {
throw new functions.https.HttpsError(
"permission-denied", "Bukan admin");
}
// Set custom claim ke target user
const { uid } = data;
await admin.auth().setCustomUserClaims(uid, { admin: true });
// Force refresh token
await admin.firestore().collection("users").doc(uid).update({
admin: true,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
});
return { success: true, message: `Admin claim ditambahkan ke ${uid}` };
});
// HTTP trigger: API endpoint
exports.apiGetUser = functions.https.onRequest(async (req, res) => {
// CORS
res.set("Access-Control-Allow-Origin", "*");
if (req.method === "OPTIONS") {
res.status(204).send("");
return;
}
const uid = req.query.uid;
if (!uid) {
res.status(400).json({ error: "uid required" });
return;
}
const userDoc = await admin.firestore()
.collection("users").doc(uid).get();
if (!userDoc.exists) {
res.status(404).json({ error: "User not found" });
return;
}
res.status(200).json(userDoc.data());
});
8. Arsitektur & State Management
Ketika aplikasi Flutter + Firebase mulai besar, penting untuk menerapkan arsitektur yang bersih dan state management yang tepat agar kode tetap terjaga maintainability-nya.
8.1 Provider Pattern
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:firebase_auth/firebase_auth.dart';
// βββββ Auth Provider βββββ
class AuthProvider extends ChangeNotifier {
final FirebaseAuth _auth = FirebaseAuth.instance;
User? _user;
bool _isLoading = true;
User? get user => _user;
bool get isLoading => _isLoading;
bool get isLoggedIn => _user != null;
AuthProvider() {
// Listen perubahan auth state
_auth.authStateChanges().listen((User? user) {
_user = user;
_isLoading = false;
notifyListeners();
});
}
Future<bool> login(String email, String password) async {
try {
await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
return true;
} catch (e) {
return false;
}
}
Future<void> logout() async {
await _auth.signOut();
}
}
// βββββ User Data Provider βββββ
class UserDataProvider extends ChangeNotifier {
final FirebaseFirestore _db = FirebaseFirestore.instance;
UserModel? _userData;
UserModel? get userData => _userData;
void startListening(String uid) {
_db.collection('users').doc(uid).snapshots().listen((doc) {
if (doc.exists) {
_userData = UserModel.fromFirestore(doc);
notifyListeners();
}
});
}
}
// βββββ Main App dengan Provider βββββ
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => UserDataProvider()),
],
child: const MyApp(),
),
);
}
8.2 Repository Pattern
// βββββ Abstract Repository βββββ
abstract class UserRepository {
Future<UserModel?> getUser(String uid);
Stream<UserModel?> userStream(String uid);
Future<void> updateUser(String uid, Map<String, dynamic> data);
}
// βββββ Firestore Implementation βββββ
class FirestoreUserRepository implements UserRepository {
final FirebaseFirestore _db = FirebaseFirestore.instance;
@override
Future<UserModel?> getUser(String uid) async {
final doc = await _db.collection('users').doc(uid).get();
return doc.exists ? UserModel.fromFirestore(doc) : null;
}
@override
Stream<UserModel?> userStream(String uid) {
return _db.collection('users').doc(uid).snapshots().map(
(doc) => doc.exists ? UserModel.fromFirestore(doc) : null,
);
}
@override
Future<void> updateUser(String uid, Map<String, dynamic> data) async {
await _db.collection('users').doc(uid).update(data);
}
}
// βββββ Mock Implementation untuk Testing βββββ
class MockUserRepository implements UserRepository {
final Map<String, UserModel> _cache = {};
@override
Future<UserModel?> getUser(String uid) async {
return _cache[uid];
}
@override
Stream<UserModel?> userStream(String uid) async* {
yield _cache[uid];
}
@override
Future<void> updateUser(String uid, Map<String, dynamic> data) async {
// Update mock data
}
}
9. Security Rules
Security Rules sangat penting untuk melindungi data Anda. Tanpa rules yang benar, data di Firestore dan Storage bisa diakses oleh siapa saja.
9.1 Firestore Security Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
function isAdmin() {
return isAuthenticated() &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
}
// βββββ Users Collection βββββ
match /users/{userId} {
// Bisa dibaca oleh semua user yang login
allow read: if isAuthenticated();
// Hanya admin atau owner yang bisa update
allow update: if isAdmin() || isOwner(userId);
// Hanya sistem yang bisa create (via Cloud Functions)
allow create: if isAdmin();
allow delete: if isAdmin();
}
// βββββ Posts Collection βββββ
match /posts/{postId} {
// Bisa dibaca semua yang login
allow read: if isAuthenticated();
// Create: harus login dan sebagai author
allow create: if isAuthenticated() &&
request.resource.data.authorId == request.auth.uid;
// Update: hanya author
allow update: if isAuthenticated() &&
resource.data.authorId == request.auth.uid;
// Delete: author atau admin
allow delete: if isAuthenticated() &&
(resource.data.authorId == request.auth.uid || isAdmin());
// Subcollection: comments
match /comments/{commentId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated();
allow update: if isAuthenticated() &&
resource.data.authorId == request.auth.uid;
allow delete: if isAuthenticated() &&
(resource.data.authorId == request.auth.uid || isAdmin());
}
}
}
}
9.2 Storage Security Rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Helper
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
// βββββ Profile Photos βββββ
match /profiles/{userId}/{allPaths=**} {
// Semua orang yang login bisa lihat foto profil
allow read: if isAuthenticated();
// Hanya owner yang bisa upload/ganti
allow write: if isOwner(userId) &&
request.resource.size < 5 * 1024 * 1024 && // Max 5MB
request.resource.contentType.matches('image/.*'); // Image only
}
// βββββ Post Images βββββ
match /posts/{userId}/{allPaths=**} {
allow read: if isAuthenticated();
allow write: if isOwner(userId) &&
request.resource.size < 10 * 1024 * 1024 && // Max 10MB
request.resource.contentType.matches('image/.*');
}
// βββββ General uploads βββββ
match /uploads/{userId}/{allPaths=**} {
allow read: if isAuthenticated();
allow write: if isOwner(userId) &&
request.resource.size < 20 * 1024 * 1024; // Max 20MB
}
}
}
10. Deploy & Monitoring
10.1 Deploy Cloud Functions
# Deploy Cloud Functions cd functions npm install cd .. firebase deploy --only functions # Deploy Security Rules firebase deploy --only firestore:rules firebase deploy --only storage:rules # Deploy Semuanya firebase deploy # Test functions lokal firebase emulators:start
10.2 Firebase Emulator Suite
import 'package:flutter/foundation.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Gunakan emulator di development
if (kDebugMode) {
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);
// FirebaseAuth.instance.useAuthEmulator('localhost', 9099);
// FirebaseStorage.instance.useStorageEmulator('localhost', 9199);
}
runApp(const MyApp());
}
10.3 Monitoring dengan Crashlytics & Analytics
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
class MonitoringService {
static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
// Inisialisasi Crashlytics
static Future<void> initialize() async {
// Catch semua errors yang tidak tertangkap
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
// Catch errors di isolate
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
// Log analytic event: app open
await _analytics.logAppOpen();
}
// Log custom event
static Future<void> logEvent({
required String name,
Map<String, dynamic>? parameters,
}) async {
await _analytics.logEvent(
name: name,
parameters: parameters,
);
}
// Log screen view
static Future<void> logScreenView(String screenName) async {
await _analytics.logScreenView(screenName: screenName);
}
// Log error ke Crashlytics
static Future<void> logError(
dynamic error,
StackTrace stack, {
String? reason,
}) async {
await FirebaseCrashlytics.instance.recordError(
error,
stack,
reason: reason,
fatal: false,
);
}
// Set user identifier (membantu debugging)
static void setUserId(String uid) {
FirebaseCrashlytics.instance.setUserIdentifier(uid);
_analytics.setUserID(id: uid);
}
}
// Contoh penggunaan:
// MonitoringService.logEvent(name: 'purchase', parameters: {
// 'item_id': 'SKU_123',
// 'price': 49.99,
// 'currency': 'IDR',
// });
- Gunakan Repository Pattern untuk memisahkan Firebase logic dari UI
- Simpan FCM tokens di Firestore dan bersihkan token yang invalid secara berkala
- Gunakan Security Rules untuk setiap collection β jangan pernah biarkan database terbuka
- Manfaatkan Firebase Emulator saat development agar tidak terkena quota
- Gunakan Cloud Functions untuk operasi yang tidak bisa dilakukan dari client (admin tasks, payment processing)
- Monitor aplikasi dengan Crashlytics & Analytics untuk menangkap error lebih awal
11. Quiz Pemahaman
Uji pemahaman Anda tentang Flutter + Firebase: