Mobile Development

Flutter + Firebase Integration: Panduan Lengkap

Tutorial lengkap integrasi Flutter dengan Firebase β€” Authentication, Firestore, Storage, Cloud Messaging, dan best practices untuk aplikasi production-ready

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 CodebaseSatu kode untuk iOS, Android, Web β€” hemat waktu development
Zero Backend SetupFirebase menyediakan auth, database, storage, push notification tanpa server
Real-timeFirestore mendukung real-time sync otomatis ke semua device
ScalableFirebase Auto Scaling menangani jutaan user tanpa konfigurasi
Google EcosystemIntegrasi mendalam dengan Google Cloud, Analytics, Crashlytics
Plugin ResmiFirebase memiliki plugin resmi FlutterFire yang maintained oleh Google

Layanan Firebase untuk Flutter

Layanan Fungsi Kasus Penggunaan
AuthenticationManajemen user & loginLogin email, Google, Apple, phone
Cloud FirestoreNoSQL database real-timeData app, chat, feed
Firebase StorageFile & media storageUpload foto, video, dokumen
Cloud MessagingPush notificationNotifikasi ke user
Cloud FunctionsServerless backendLogic server-side, scheduled tasks
AnalyticsAnalytics & CrashlyticsMonitoring performa & error
Diagram: Arsitektur Flutter + Firebase
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    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

  1. Buka Firebase Console
  2. Klik "Add project" dan masukkan nama project
  3. Enable/disable Google Analytics sesuai kebutuhan
  4. Klik "Create project"

Langkah 2: Install FlutterFire CLI

Terminal β€” Install FlutterFire
# 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:

Langkah 3: Tambahkan Dependencies

YAML β€” pubspec.yaml
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()

Dart β€” main.dart
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();
      },
    );
  }
}
⚠️ Penting: Inisialisasi Firebase

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

Dart β€” services/auth_service.dart
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

Dart β€” Google Sign-In
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

Dart β€” Phone Authentication
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

Dart β€” Login Page
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.

Diagram: Struktur Firestore
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              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

Dart β€” models/user_model.dart
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

Dart β€” services/firestore_service.dart
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

Dart β€” Real-time User List
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'),
              );
            },
          );
        },
      ),
    );
  }
}
πŸ’‘ Tips: Offline Persistence

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

Dart β€” services/storage_service.dart
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

Dart β€” Upload Widget
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

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());
// }

6.3 Kirim Notifikasi via Cloud Functions

JavaScript β€” Cloud Functions (functions/index.js)
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

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.

8.1 Provider Pattern

Dart β€” Provider + Firebase
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

Dart β€” 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

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

10.2 Firebase Emulator Suite

Dart β€” Menggunakan Emulator saat Development
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

Dart β€” Monitoring Setup
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',
// });
πŸ’‘ Best Practices Flutter + Firebase
  • 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:

Pertanyaan 1: Apa yang harus dilakukan sebelum runApp() saat menggunakan Firebase?

a) Tidak perlu melakukan apa-apa
b) Panggil Firebase.initializeApp()
c) Import semua Firebase packages
d) Buat akun Firebase terlebih dahulu

Pertanyaan 2: Bagaimana cara listen perubahan real-time dari Firestore?

a) Menggunakan .get() berulang kali
b) Menggunakan .snapshots() yang mengembalikan Stream
c) Menggunakan polling setiap detik
d) Menggunakan Cloud Functions

Pertanyaan 3: Mengapa FCM background handler harus berupa top-level function?

a) Karena itu adalah convention saja
b) Karena isolate Flutter berbeda β€” tidak bisa mengakses instance class
c) Karena Firebase tidak support class
d) Karena Dart tidak support nested function

Pertanyaan 4: Apa manfaat menggunakan batch write di Firestore?

a) Membuat data lebih aman
b) Semua operasi berjalan atomic β€” berhasil semua atau gagal semua
c) Menghemat bandwidth
d) Mempercepat query

Pertanyaan 5: Berapa maksimal ukuran file yang bisa di-upload ke Firebase Storage dengan rules di atas?

a) 1MB untuk semua
b) Profile: 5MB, Posts: 10MB, Uploads: 20MB
c) 100MB untuk semua
d) Tanpa batas
πŸ” Zoom
100%
🎨 Tema