blocuibuilderMahir4 mnt baca

BLoC di UI: Builder, Listener, Consumer

Tiga widget BLoC: kapan rebuild, kapan efek samping.

Kenapa Dipisah: Rebuild vs Efek Samping

BLoC punya satu masalah klasik yang sering bikin bug misterius: ketika state berubah, ada dua hal berbeda yang harus terjadi. Pertama, UI harus digambar ulang (rebuild) supaya tampilan sinkron dengan data. Kedua, efek samping harus dijalankan tepat satu kali, seperti navigasi ke halaman sukses, menampilkan SnackBar, atau membuka dialog. Kalau keduanya dicampur dalam satu callback, efek samping bisa terpanggil berkali-kali setiap Flutter me-rebuild, dan user bisa tiba-tiba di-navigasi dua kali atau dapat dua SnackBar bertumpuk. Karena itulah flutter_bloc menyediakan tiga widget dengan peran berbeda: BlocBuilder untuk render, BlocListener untuk efek samping, dan BlocConsumer sebagai gabungan keduanya.

BlocBuilder: Untuk Render UI

BlocBuilder memanggil builder-nya setiap kali state baru di-emit, lalu me-rebuild subtree di bawahnya. Aturan emasnya: builder harus murni. Ia hanya boleh mengembalikan widget berdasarkan state, tidak boleh navigasi, tidak boleh showDialog, dan tidak boleh memicu event cubit lain. Alasannya, builder bisa dipanggil berkali-kali bahkan tanpa state baru (misalnya saat widget parent rebuild), sehingga efek samping di dalamnya bisa terpicu ganda tanpa kamu sadari.

dart
BlocBuilder<CartCubit, CartState>(
  buildWhen: (prev, curr) => curr is CartLoaded || curr is CartLoading,
  builder: (context, state) {
    if (state is CartLoading) {
      return const Center(child: CircularProgressIndicator());
    }
    if (state is CartLoaded) {
      if (state.items.isEmpty) {
        return const Center(child: Text('Keranjang masih kosong'));
      }
      return ListView.builder(
        itemCount: state.items.length,
        itemBuilder: (_, i) => CartTile(item: state.items[i]),
      );
    }
    return const SizedBox.shrink();
  },
)

Perhatikan buildWhen: ia memfilter state mana yang boleh memicu rebuild. Tanpa ini, setiap state (termasuk state transien seperti CartItemAdded) akan me-rebuild list, dan itu pemborosan.

BlocListener: Untuk Efek Samping

BlocListener tidak me-rebuild apa pun. Ia hanya memanggil listener satu kali setiap kali state baru datang. Inilah tempat yang benar untuk navigasi, SnackBar, dialog, dan event analytics:

dart
BlocListener<CheckoutCubit, CheckoutState>(
  listenWhen: (prev, curr) =>
      curr is CheckoutSuccess || curr is CheckoutFailure,
  listener: (context, state) {
    if (state is CheckoutSuccess) {
      Navigator.pushReplacementNamed(context, '/sukses',
          arguments: state.orderId);
    } else if (state is CheckoutFailure) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.pesan)),
      );
    }
  },
  child: const CheckoutForm(),
)

Untuk alur yang butuh keduanya sekaligus, bungkus dengan BlocConsumer (punya listener dan builder dalam satu widget), atau tumpuk BlocListener di atas BlocBuilder. Pola tumpuk lebih disukai saat listener dan builder butuh filter When yang berbeda.

Edge Cases yang Sering Menjebak

Pertama, state yang di-emit dua kali berturut-turut dengan isi sama tetap memicu listener dua kali, karena BLoC membandingkan state secara default memakai ==. Solusinya: buat state extends Equatable dan isi props dengan benar, supaya state yang setara dianggap sama dan tidak memicu rebuild sia-sia. Kedua, ScaffoldMessenger.of(context) di dalam listener bisa throw kalau context yang dipakai bukan descendant dari Scaffold. Pastikan listener berada di bawah Scaffold, atau pindahkan pemanggilan SnackBar ke widget yang sudah pasti punya Scaffold. Ketiga, hati-hati memakai context.read di dalam builder untuk memicu event: itu pola yang rapuh karena builder bisa terpanggil berulang. Pemicu event yang aman adanya di initState, onPressed, atau listener.

Kapan TIDAK Pakai

Jangan pakai trio ini untuk state lokal sederhana seperti show/hide password, tab yang dipilih, atau expand/collapse kartu. Itu ranahnya setState atau ValueListenableBuilder. Widget BLoC baru masuk akal untuk state yang lintas layar, butuh di-test, atau punya alur efek samping yang kompleks. Memaksakan BlocBuilder untuk semua hal bikin kode berisik: setiap toggle kecil jadi butuh cubit, state class, dan boilerplate yang tidak perlu.

Catatan teknis: Kalau kamu butuh membaca state BLoC sekali saja tanpa rebuild (misalnya di initState atau di dalam onPressed), pakai context.read<CartCubit>(). context.watch dan BlocBuilder selalu ikut rebuild saat state berubah, jadi jangan dipakai di tempat yang tidak butuh rebuild.

Tantangan

Listener navigasi

Dengan LoginCubit (state: LoginSukses/LoginGagal), tulis BlocListener yang: navigasi ke '/home' saat sukses, tampilkan SnackBar pesan error saat gagal.

Kuis Bab

Uji pemahamanmu: State Management Lanjutan

Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".

1.Apa perbedaan ref.watch dan ref.read di Riverpod?

2.Kapan memakai FutureProvider?

3.Apa aturan emas saat mengubah state di Notifier?

4.Apa perbedaan Cubit dan Bloc penuh?

5.Di widget BLoC apa efek samping (navigasi, snackbar) seharusnya ditaruh?