Skip to content

Bab 19 — Provider

Provider adalah paket state management yang mendahului Riverpod — dibuat oleh pengembang yang sama. Ia masih sangat banyak dipakai di proyek yang sudah berjalan, dan memahaminya penting kalau kamu bergabung ke tim yang memakainya.

Bab ini lebih ringkas daripada Riverpod, karena banyak konsepnya serupa. Fokus kita adalah perbedaannya.

Memasang

yaml
# pubspec.yaml
dependencies:
  provider: ^6.1.2

ChangeNotifier: fondasinya

Berbeda dari Riverpod yang memakai StateNotifier, Provider paling sering dipasangkan dengan ChangeNotifier — kelas bawaan Flutter yang bisa memberi tahu pendengarnya ketika sesuatu berubah.

dart
// lib/providers/penyedia_polling.dart
import 'package:flutter/material.dart';
import '../models/polling.dart';
import '../services/layanan_polling.dart';

class PenyediaPolling extends ChangeNotifier {
  PenyediaPolling(this._layanan);

  final LayananPolling _layanan;

  List<Polling> _daftarPolling = [];
  bool _sedangMemuat = false;
  String? _pesanKesalahan;

  // Getter publik — widget hanya bisa membaca, tidak mengubah langsung
  List<Polling> get daftarPolling => List.unmodifiable(_daftarPolling);
  bool get sedangMemuat => _sedangMemuat;
  String? get pesanKesalahan => _pesanKesalahan;
  int get jumlahPolling => _daftarPolling.length;

  Future<void> muatPolling() async {
    _sedangMemuat = true;
    _pesanKesalahan = null;
    notifyListeners();          // ← beri tahu UI: mulai memuat

    try {
      _daftarPolling = await _layanan.ambilSemua();
    } catch (kesalahan) {
      _pesanKesalahan = 'Gagal memuat polling. Coba lagi.';
    } finally {
      _sedangMemuat = false;
      notifyListeners();        // ← beri tahu UI: selesai
    }
  }

  Future<void> tambahPolling(Polling polling) async {
    final tersimpan = await _layanan.simpan(polling);
    _daftarPolling = [tersimpan, ..._daftarPolling];
    notifyListeners();
  }

  Future<void> hapusPolling(String id) async {
    // Optimistic update — hapus dulu, kembalikan kalau gagal
    final cadangan = _daftarPolling;
    _daftarPolling = _daftarPolling.where((p) => p.id != id).toList();
    notifyListeners();

    try {
      await _layanan.hapus(id);
    } catch (_) {
      _daftarPolling = cadangan;
      _pesanKesalahan = 'Gagal menghapus polling.';
      notifyListeners();
    }
  }

  Future<void> beriSuara(String idPolling, int indeksOpsi) async {
    final indeks = _daftarPolling.indexWhere((p) => p.id == idPolling);
    if (indeks == -1) return;

    final polling = _daftarPolling[indeks];
    final diperbarui = polling.dengansuara(indeksOpsi);

    // Buat list baru dengan satu elemen diganti
    _daftarPolling = [
      for (final p in _daftarPolling) p.id == idPolling ? diperbarui : p,
    ];
    notifyListeners();

    await _layanan.perbarui(diperbarui);
  }
}

Perbedaan mendasar dari StateNotifier:

ChangeNotifier (Provider)StateNotifier (Riverpod)
Cara memberi tahunotifyListeners() manualOtomatis saat state = ...
Struktur stateBanyak properti terpisahSatu objek state
MutabilitasProperti boleh diubah langsungHarus ganti seluruh objek

ChangeNotifier lebih longgar — kamu bisa mengubah properti langsung, asal ingat memanggil notifyListeners(). Kelonggaran itu sekaligus jebakannya: lupa memanggil notifyListeners() adalah bug nomor satu di Provider, dan tidak ada error apa pun yang muncul — UI hanya diam saja.

Kenapa List.unmodifiable

Getter daftarPolling mengembalikan salinan yang tidak bisa diubah. Tanpa itu, widget bisa memanggil penyedia.daftarPolling.add(...) dan mengubah state tanpa memicu notifyListeners() — menghasilkan bug yang sangat sulit dilacak.

Mendaftarkan provider

dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/penyedia_polling.dart';
import 'providers/penyedia_auth.dart';
import 'providers/penyedia_tema.dart';
import 'services/layanan_polling.dart';

void main() {
  runApp(const AplikasiSaya());
}

class AplikasiSaya extends StatelessWidget {
  const AplikasiSaya({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // Provider biasa — untuk objek yang tidak berubah
        Provider<LayananPolling>(
          create: (_) => LayananPolling(),
        ),

        // ChangeNotifierProvider — untuk state yang berubah
        ChangeNotifierProvider<PenyediaAuth>(
          create: (_) => PenyediaAuth(),
        ),

        ChangeNotifierProvider<PenyediaTema>(
          create: (_) => PenyediaTema(),
        ),

        // ProxyProvider — provider yang bergantung pada provider lain
        ChangeNotifierProxyProvider<LayananPolling, PenyediaPolling>(
          create: (ctx) => PenyediaPolling(
            ctx.read<LayananPolling>(),
          ),
          update: (ctx, layanan, sebelumnya) =>
              sebelumnya ?? PenyediaPolling(layanan),
        ),
      ],
      child: Consumer<PenyediaTema>(
        builder: (context, tema, _) {
          return MaterialApp(
            title: 'Aplikasi Polling',
            theme: TemaAplikasi.terang(),
            darkTheme: TemaAplikasi.gelap(),
            themeMode: tema.mode,
            home: const LayarBeranda(),
          );
        },
      ),
    );
  }
}

Beberapa hal yang perlu diperhatikan:

MultiProvider menghindari widget yang bersarang berlapis-lapis. Tanpa itu, kamu akan menulis ChangeNotifierProvider di dalam ChangeNotifierProvider di dalam Provider.

Urutan penting. Provider yang bergantung pada provider lain harus dideklarasikan setelah yang menjadi ketergantungannya.

ChangeNotifierProxyProvider untuk provider yang butuh provider lain. Parameter update dipanggil setiap kali ketergantungannya berubah; mengembalikan sebelumnya ?? ... mencegah pembuatan ulang yang tidak perlu.

Membaca provider

Ada beberapa cara, dan memilih yang tepat sama pentingnya seperti di Riverpod.

context.watch — berlangganan

dart
@override
Widget build(BuildContext context) {
  final penyedia = context.watch<PenyediaPolling>();

  if (penyedia.sedangMemuat) {
    return const Center(child: CircularProgressIndicator());
  }

  return ListView.builder(
    itemCount: penyedia.daftarPolling.length,
    itemBuilder: (ctx, i) => KartuPolling(polling: penyedia.daftarPolling[i]),
  );
}

Widget ini akan dibangun ulang setiap kali notifyListeners() dipanggil.

context.read — baca sekali

dart
ElevatedButton(
  onPressed: () {
    context.read<PenyediaPolling>().muatPolling();
  },
  child: const Text('Muat Ulang'),
)

Sama seperti Riverpod: watch di build, read di callback.

Jangan read di build, jangan watch di callback

dart
// ❌ UI tidak akan pernah diperbarui
final penyedia = context.read<PenyediaPolling>();

// ❌ Error: watch dipanggil di luar build
onPressed: () => context.watch<PenyediaPolling>().muat(),

context.select — berlangganan sebagian

dart
// Hanya dibangun ulang kalau jumlahnya berubah,
// bukan setiap kali isi daftar berubah
final jumlah = context.select<PenyediaPolling, int>(
  (p) => p.daftarPolling.length,
);

return Text('$jumlah polling');

Consumer — membatasi cakupan rebuild

dart
class LayarBeranda extends StatelessWidget {
  const LayarBeranda({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // AppBar ini tidak pernah dibangun ulang
      appBar: AppBar(
        title: const Text('Polling'),
        actions: [
          // Hanya lencana ini yang berlangganan
          Consumer<PenyediaPolling>(
            builder: (context, penyedia, child) {
              return Badge(
                label: Text('${penyedia.jumlahPolling}'),
                child: child,
              );
            },
            child: const Icon(Icons.poll),
          ),
        ],
      ),

      // Hanya body ini yang berlangganan
      body: Consumer<PenyediaPolling>(
        builder: (context, penyedia, _) {
          if (penyedia.sedangMemuat) {
            return const Center(child: CircularProgressIndicator());
          }

          if (penyedia.pesanKesalahan != null) {
            return Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(penyedia.pesanKesalahan!),
                  const SizedBox(height: 12),
                  ElevatedButton(
                    onPressed: penyedia.muatPolling,
                    child: const Text('Coba Lagi'),
                  ),
                ],
              ),
            );
          }

          if (penyedia.daftarPolling.isEmpty) {
            return const Center(child: Text('Belum ada polling.'));
          }

          return RefreshIndicator(
            onRefresh: penyedia.muatPolling,
            child: ListView.builder(
              itemCount: penyedia.daftarPolling.length,
              itemBuilder: (ctx, i) => KartuPolling(
                polling: penyedia.daftarPolling[i],
                saatBeriSuara: (indeksOpsi) => context
                    .read<PenyediaPolling>()
                    .beriSuara(penyedia.daftarPolling[i].id, indeksOpsi),
              ),
            ),
          );
        },
      ),

      // FAB ini juga tidak pernah dibangun ulang
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.of(context).push(
          MaterialPageRoute(builder: (_) => const LayarBuatPolling()),
        ),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Parameter child pada Consumer bekerja sama seperti di AnimatedBuilder: ia dibuat sekali dan diteruskan tanpa dibangun ulang.

Selector — kendali paling halus

dart
Selector<PenyediaPolling, int>(
  selector: (context, penyedia) => penyedia.jumlahPolling,
  builder: (context, jumlah, child) {
    return Text('$jumlah polling');
  },
)

Selector hanya membangun ulang ketika nilai yang dipilih benar-benar berubah, dibandingkan dengan ==.

Memuat data saat layar dibuka

Ini titik yang sering menimbulkan kebingungan. Kamu tidak boleh memanggil notifyListeners() selama fase pembangunan widget.

dart
class LayarPolling extends StatefulWidget {
  const LayarPolling({super.key});

  @override
  State<LayarPolling> createState() => _LayarPollingState();
}

class _LayarPollingState extends State<LayarPolling> {
  @override
  void initState() {
    super.initState();

    // ❌ Error: notifyListeners dipanggil saat widget sedang dibangun
    // context.read<PenyediaPolling>().muatPolling();

    // ✅ Tunda sampai frame pertama selesai
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<PenyediaPolling>().muatPolling();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<PenyediaPolling>(
      builder: (context, penyedia, _) {
        // ...
      },
    );
  }
}

Alternatif yang lebih bersih

Beberapa tim lebih memilih memuat data di dalam konstruktor provider:

dart
class PenyediaPolling extends ChangeNotifier {
  PenyediaPolling(this._layanan) {
    muatPolling();   // langsung muat saat dibuat
  }
  // ...
}

Ini menghilangkan kebutuhan addPostFrameCallback sama sekali, dan membuat widget tidak perlu tahu apa-apa tentang pemuatan data.

FutureProvider dan StreamProvider

Provider juga punya padanan untuk data asinkron:

dart
MultiProvider(
  providers: [
    FutureProvider<List<Polling>>(
      create: (ctx) => ctx.read<LayananPolling>().ambilSemua(),
      initialData: const [],
    ),

    StreamProvider<User?>(
      create: (_) => FirebaseAuth.instance.authStateChanges(),
      initialData: null,
    ),
  ],
  child: const AplikasiSaya(),
)
dart
final daftar = context.watch<List<Polling>>();
final pengguna = context.watch<User?>();

Keduanya butuh initialData, dan tidak menyediakan penanganan loading/error sebaik AsyncValue milik Riverpod. Untuk data asinkron yang kompleks, banyak tim tetap memakai FutureBuilder bersama ChangeNotifier, atau beralih ke Riverpod.

Contoh: penyedia autentikasi

Pola yang sangat umum di aplikasi nyata.

dart
// lib/providers/penyedia_auth.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';

enum StatusAuth { belumDitentukan, masuk, keluar }

class PenyediaAuth extends ChangeNotifier {
  PenyediaAuth() {
    // Dengarkan perubahan status login dari Firebase
    _langganan = FirebaseAuth.instance.authStateChanges().listen((pengguna) {
      _pengguna = pengguna;
      _status = pengguna == null ? StatusAuth.keluar : StatusAuth.masuk;
      notifyListeners();
    });
  }

  StreamSubscription<User?>? _langganan;

  User? _pengguna;
  StatusAuth _status = StatusAuth.belumDitentukan;
  bool _sedangProses = false;
  String? _pesanKesalahan;

  User? get pengguna => _pengguna;
  StatusAuth get status => _status;
  bool get sedangProses => _sedangProses;
  String? get pesanKesalahan => _pesanKesalahan;
  bool get sudahMasuk => _pengguna != null;

  Future<bool> masuk(String email, String sandi) async {
    _sedangProses = true;
    _pesanKesalahan = null;
    notifyListeners();

    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email.trim(),
        password: sandi,
      );
      return true;
    } on FirebaseAuthException catch (e) {
      _pesanKesalahan = switch (e.code) {
        'user-not-found' => 'Akun tidak ditemukan.',
        'wrong-password' => 'Kata sandi salah.',
        'invalid-email' => 'Format email tidak valid.',
        'invalid-credential' => 'Email atau kata sandi salah.',
        'too-many-requests' => 'Terlalu banyak percobaan. Coba lagi nanti.',
        _ => 'Gagal masuk. Coba lagi.',
      };
      return false;
    } finally {
      _sedangProses = false;
      notifyListeners();
    }
  }

  Future<void> keluar() async {
    await FirebaseAuth.instance.signOut();
    // _pengguna diperbarui otomatis lewat authStateChanges
  }

  @override
  void dispose() {
    _langganan?.cancel();   // ⚠️ wajib, kalau tidak memori bocor
    super.dispose();
  }
}

Dipakai untuk mengatur layar mana yang tampil:

dart
class GerbangAuth extends StatelessWidget {
  const GerbangAuth({super.key});

  @override
  Widget build(BuildContext context) {
    final auth = context.watch<PenyediaAuth>();

    return switch (auth.status) {
      StatusAuth.belumDitentukan => const LayarSplash(),
      StatusAuth.masuk => const LayarBeranda(),
      StatusAuth.keluar => const LayarMasuk(),
    };
  }
}

Perhatikan dispose() yang membatalkan langganan stream. ChangeNotifier punya dispose() sendiri yang dipanggil ketika provider dibuang, dan melewatkannya adalah penyebab kebocoran memori yang umum.

Provider versus Riverpod

Sekarang setelah melihat keduanya, berikut perbandingannya secara langsung.

AspekProviderRiverpod
Butuh BuildContextYaTidak
Kesalahan tipe terdeteksiSaat runtimeSaat kompilasi
Provider ganda dengan tipe samaSulitMudah
TestingButuh widget treeBisa tanpa widget
Provider bergantung providerProxyProvider (verbose)ref.watch (alami)
Penanganan asyncManualAsyncValue bawaan
BoilerplateSedikit lebih sedikitSedikit lebih banyak
KematanganSangat matangMatang, aktif dikembangkan

Kelemahan terbesar Provider yang diselesaikan Riverpod:

dart
// Provider: kalau tipe tidak terdaftar, error baru muncul saat runtime
final penyedia = context.watch<PenyediaYangLupaDidaftarkan>();
// 💥 ProviderNotFoundException saat aplikasi berjalan

// Riverpod: provider adalah variabel, compiler menangkap kesalahannya
final nilai = ref.watch(providerYangSalahKetik);
// ❌ Error saat kompilasi — nama tidak dikenal

Juga, Provider tidak bisa punya dua provider dengan tipe yang sama:

dart
// ❌ Provider mana yang diambil? Tidak jelas.
Provider<String>(create: (_) => 'nama'),
Provider<String>(create: (_) => 'alamat'),

Di Riverpod ini bukan masalah, karena setiap provider adalah variabel dengan nama sendiri.

Kapan tetap memakai Provider

  • Proyekmu sudah memakainya dan bekerja dengan baik. Migrasi besar-besaran jarang sepadan.
  • Tim kamu sudah terbiasa dan tidak ingin belajar API baru.
  • Aplikasimu sederhana dan ChangeNotifier sudah cukup.

Untuk proyek baru, sebagian besar rekomendasi saat ini mengarah ke Riverpod atau BLoC.

Latihan Mandiri

Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.

Variasi 1: Daftar Belanja dengan ChangeNotifier — ⭐⭐ · 40–60 menit

Tantangan: Ambil latihan daftar belanja dari bab sebelumnya — atau buat ulang dari nol — lalu implementasikan dengan Provider dan ChangeNotifier. Setelah selesai, tulis satu paragraf untuk dirimu sendiri: apa yang terasa lebih mudah, dan apa yang terasa lebih rawan salah dibandingkan Riverpod.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│  ┌─────────────────────────────┐  │
│  │ ☐ Susu                       │  │
│  │ ☑ Roti                       │  │
│  │ ☐ Telur                      │  │
│  └─────────────────────────────┘  │
│       2 dari 3 sudah dibeli        │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Sengaja hapus satu notifyListeners() → centang di atas berubah di data, tapi tampilannya diam saja (tidak error, tidak peringatan).

Kriteria selesai:

  • ChangeNotifier memanggil notifyListeners() di setiap tempat state berubah — dan kamu sudah membuktikannya dengan sengaja menghapus satu panggilan lalu mengamati apa yang terjadi.
  • Getter daftar mengembalikan List.unmodifiable, sehingga widget tidak bisa mengubah state diam-diam.
  • Provider didaftarkan lewat MultiProvider, dan context.watch hanya dipakai di build sementara context.read hanya di callback.
  • Ringkasan "sekian dari sekian" memakai Selector atau context.select, bukan watch pada seluruh objek.

Petunjuk: Kriteria pertama adalah inti latihan ini. Menghapus satu notifyListeners() tidak menghasilkan peringatan, tidak menghasilkan error, dan kodenya tetap terlihat benar — UI hanya diam. Rasakan sekali, dan kamu akan selamanya ingat kenapa Riverpod menghilangkan langkah ini. Kalau kamu mendapat error tentang notifyListeners yang dipanggil saat widget sedang dibangun, itu biasanya karena memuat data di dalam build; pindahkan ke konstruktor provider-nya. Dan kalau memakai ChangeNotifierProxyProvider, kembalikan sebelumnya ?? ... supaya objeknya tidak dibuat ulang setiap kali ketergantungannya berubah.

Variasi 2: Tiket Wahana Bertingkat — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun pemesanan tiket taman hiburan dengan dua provider yang saling bergantung: satu memegang keanggotaan pengguna (biasa, perak, emas), satu lagi memegang keranjang tiket. Harga di keranjang harus otomatis menyesuaikan diskon keanggotaan setiap kali tingkatnya berubah.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│  Keanggotaan: Perak (diskon 10%)   │
│  ┌─────────────────────────────┐  │
│  │ Tiket Dewasa x2   Rp 180.000 │  │
│  │ Tiket Anak x1     Rp  45.000 │  │
│  │ Diskon Perak (10%) -Rp22.500 │  │
│  │ TOTAL             Rp 202.500 │  │
│  └─────────────────────────────┘  │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Ganti keanggotaan ke "Emas" → TOTAL berubah seketika, tapi isi keranjang (Dewasa x2, Anak x1) tidak ikut hilang atau ter-reset.

Kriteria selesai:

  • Keranjang menerima data keanggotaan lewat ChangeNotifierProxyProvider, bukan dengan membaca provider lain lewat context di dalam metodenya.
  • Mengubah tingkat keanggotaan langsung memperbarui total keranjang tanpa pengguna menyentuh apa pun.
  • Provider dideklarasikan dalam urutan yang benar di MultiProvider.
  • Objek keranjang tidak dibuat ulang setiap kali keanggotaan berubah — isinya tetap.

Petunjuk: Kriteria terakhir adalah jebakan ChangeNotifierProxyProvider: fungsi update dipanggil setiap kali ketergantungannya berubah, dan kalau kamu selalu mengembalikan objek baru di sana, isi keranjang pengguna akan terhapus setiap kali keanggotaannya berubah. Mengembalikan objek sebelumnya kalau sudah ada — lalu memperbaruinya — adalah pola yang benar. Untuk urutan di MultiProvider, ingat bahwa yang di bawah bisa melihat yang di atas, tidak sebaliknya.

Variasi 3: Sesi Perpustakaan — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun provider sesi anggota perpustakaan yang mendengarkan sebuah Stream status sesi (masuk, keluar, sesi kedaluwarsa), lalu memakainya untuk menentukan layar mana yang tampil. Sesi yang kedaluwarsa harus langsung mengeluarkan pengguna dari layar mana pun ia berada.

Bayangan alur layar:

text
Stream status sesi:
  masuk        ──▶ [Layar Beranda]
  keluar       ──▶ [Layar Masuk]
  kedaluwarsa  ──▶ [Layar Masuk]  (dari layar mana pun, tanpa
                                    layar lama tertinggal di tumpukan)

Saat aplikasi dibuka:
  [Splash] ──(sesi dipulihkan)──▶ [Beranda] atau [Masuk]
  (mencegah Layar Masuk "berkedip" sesaat sebelum Beranda muncul)

Kriteria selesai:

  • Provider berlangganan stream di konstruktornya, dan membatalkan langganan di dispose().
  • Perpindahan layar terjadi secara reaktif dari state, bukan lewat push manual di dalam callback.
  • Sesi kedaluwarsa saat pengguna berada di layar detail mengembalikannya ke layar masuk tanpa meninggalkan layar lama di tumpukan.
  • Layar splash mencegah layar masuk berkedip saat sesi sedang dipulihkan.

Petunjuk: ChangeNotifier punya dispose() seperti State, dan melewatkan pembatalan langganan di sana adalah kebocoran yang sama persis bentuknya — stream tetap hidup, callback-nya tetap dipanggil, dan notifyListeners() dijalankan pada objek yang sudah dibuang. Untuk navigasi reaktif, bangun satu widget gerbang yang mengawasi state sesi dan mengembalikan layar yang sesuai; pola ini akan muncul lagi hampir identik di Bab 28.

Variasi 4: Papan Skor Langsung — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun papan skor pertandingan yang diperbarui setiap detik: skor, waktu berjalan, jumlah pelanggaran, dan daftar kejadian. Yang diuji di sini adalah membatasi rebuild — memperbarui waktu tidak boleh membangun ulang daftar kejadian.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│  Tim A   2 - 1   Tim B             │
│  ⏱ 45:12                           │
│  Pelanggaran: 3                    │
│  ┌─────────────────────────────┐  │
│  │ 12' Gol - Tim A               │  │
│  │ 34' Kartu Kuning - Tim B      │  │
│  └─────────────────────────────┘  │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Hanya baris ⏱ 45:12 yang rebuild setiap detik — skor, pelanggaran, dan daftar kejadian di atas diam kecuali nilainya benar-benar berubah.

Kriteria selesai:

  • Kamu sudah mengukur berapa banyak widget yang dibangun ulang per detik sebelum optimasi, dengan debugPrint di build().
  • Setelah optimasi, hanya widget waktu yang dibangun ulang per detik.
  • Selector atau context.select dipakai untuk memisahkan langganan per properti.
  • Parameter child pada Consumer dipakai untuk bagian yang tidak pernah berubah.

Petunjuk: context.watch membangun ulang seluruh widget yang memanggilnya, sehingga menaruhnya di build milik Scaffold berarti seluruh layar dibangun ulang setiap detik. Selector mempersempit dua hal sekaligus: bagian state mana yang didengarkan, dan bagian pohon mana yang dibangun ulang. Parameter child adalah tambahan yang gratis — apa pun yang kamu taruh di sana dibangun sekali lalu dioper apa adanya.

Variasi 5: Pindah ke Riverpod — ⭐⭐⭐⭐ · 60–90 menit

Tantangan: Ambil salah satu latihan Provider di atas dan tulis ulang dengan Riverpod, tanpa mengubah perilaku yang terlihat pengguna. Lalu tulis perbandingan jujurmu sendiri dalam satu halaman.

Kriteria selesai:

  • Seluruh ChangeNotifier diganti dengan StateNotifier atau AsyncNotifier, dan tidak ada satu pun notifyListeners() yang tersisa.
  • Tidak ada provider yang membutuhkan BuildContext untuk diakses dari lapisan logika.
  • Kamu membuat dua provider bertipe sama dan membuktikan keduanya bisa hidup berdampingan — sesuatu yang tidak bisa dilakukan Provider.
  • Perbandinganmu menyebut minimal satu hal yang justru lebih baik di Provider.

Petunjuk: Kriteria ketiga adalah perbedaan yang paling sulit dirasakan sampai kamu membutuhkannya: Provider mencari berdasarkan tipe, sehingga dua ChangeNotifier bertipe sama dalam satu pohon membuatnya bingung. Riverpod mencari berdasarkan identitas objek providernya, sehingga masalah itu tidak ada. Untuk kriteria terakhir, jawaban yang jujur biasanya berkisar pada seberapa cepat Provider bisa dipahami orang yang baru bergabung — kesederhanaan punya nilainya sendiri.

Ikhtisar

  • Provider paling sering dipasangkan dengan ChangeNotifier, dan kamu harus memanggil notifyListeners() secara manual setiap kali state berubah.
  • Lupa notifyListeners() adalah bug nomor satu — tidak ada error, UI hanya diam.
  • Kembalikan List.unmodifiable dari getter agar widget tidak bisa mengubah state diam-diam.
  • Daftarkan dengan MultiProvider; urutan penting karena provider yang bergantung harus dideklarasikan setelah ketergantungannya.
  • ChangeNotifierProxyProvider untuk provider yang butuh provider lain — kembalikan sebelumnya ?? ... agar tidak dibuat ulang percuma.
  • context.watch di build, context.read di callback, context.select untuk berlangganan satu properti.
  • Consumer dan Selector membatasi cakupan rebuild; parameter child untuk bagian yang tidak berubah.
  • Jangan panggil notifyListeners() saat widget sedang dibangun — pakai addPostFrameCallback, atau lebih baik muat data di konstruktor provider.
  • Selalu batalkan StreamSubscription di dispose() milik ChangeNotifier.
  • Riverpod menyelesaikan tiga kelemahan Provider: kesalahan terdeteksi saat kompilasi, tidak butuh BuildContext, dan bisa punya banyak provider bertipe sama.

Berikutnya: Bab 20 — BLoC: Konsep Inti, pendekatan yang berbeda secara mendasar.

Transkrip asli

Disintesis dari 4_flutter_ai-chatbot-n-firebase/5_state-management-with-provider.md (3 video: Introduction to State Management, State Management Using Provider Part 1 & 2). Lihat PDF Firebase & AI.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.