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 |
UI Layer (Widgets)
State Mgmt (Provider/ Riverpod) Firebase Services
Cloud Messaging
Authentication
Firestore ◄
Storage
Auth Firestore
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
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: