Tutorial lengkap integrasi Flutter dengan Firebase — Authentication, Firestore, Storage, Cloud Messaging, dan best practices untuk aplikasi production-ready
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
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.
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
Dart — services/messaging_service.dart
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)
Dart — background_handler.dart (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());
// }
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
Dart — Memanggil Cloud Function
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
JavaScript — Callable Function
// 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.
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
Firestore Rules — firestore.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
Storage Rules — storage.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
Terminal — Deploy
# 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