Skip to content

Bab 21 — flutter_bloc: Widget Penghubung

Bab lalu membangun Cubit dan Bloc. Bab ini membahas lapisan yang menghubungkannya ke antarmuka: empat widget dari paket flutter_bloc, dan BuildContext yang menjadi jembatannya.

Memilih widget yang tepat di antara keempatnya adalah keterampilan yang membedakan kode BLoC yang bersih dari yang berantakan.

Empat widget, empat tugas

┌───────────────────────────────────────────────────────────────┐
│  BlocProvider                                                 │
│  Membuat & menyediakan instance ke widget di bawahnya         │
│  → dipakai sekali, di atas cabang yang membutuhkannya         │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│  BlocBuilder                                                  │
│  Membangun ulang UI saat state berubah                        │
│  → untuk MENAMPILKAN state                                    │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│  BlocListener                                                 │
│  Menjalankan efek samping saat state berubah, TANPA rebuild   │
│  → snackbar, dialog, navigasi                                 │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│  BlocConsumer                                                 │
│  Gabungan Builder + Listener dalam satu widget                │
│  → kalau butuh keduanya di tempat yang sama                   │
└───────────────────────────────────────────────────────────────┘

Aturan sederhana untuk memilih:

  • Perlu menggambar sesuatu dari state? → BlocBuilder
  • Perlu melakukan sesuatu (snackbar, pindah halaman)? → BlocListener
  • Perlu keduanya? → BlocConsumer

BlocProvider

Membuat instance BLoC dan menyediakannya untuk seluruh cabang di bawahnya.

dart
BlocProvider<CounterCubit>(
  create: (context) => CounterCubit(),
  child: const HomeScreen(),
)

Dua hal penting tentang perilakunya:

create dipanggil malas. Cubit baru dibuat ketika pertama kali diakses, bukan saat BlocProvider dibangun. Kalau kamu butuh ia langsung aktif, tambahkan lazy: false:

dart
BlocProvider<AuthCubit>(
  create: (context) => AuthCubit()..periksaStatusLogin(),
  lazy: false,   // langsung jalan, jangan tunggu diakses
  child: const AplikasiSaya(),
)

BlocProvider otomatis memanggil close(). Ketika BlocProvider dilepas dari pohon, cubit di dalamnya dibuang dan stream-nya ditutup. Kamu tidak perlu mengurusnya sendiri.

MultiBlocProvider

Kalau butuh beberapa BLoC di level yang sama, jangan menyarangkan BlocProvider berlapis-lapis:

dart
// ❌ Sulit dibaca
BlocProvider<AuthCubit>(
  create: (_) => AuthCubit(),
  child: BlocProvider<ThemeCubit>(
    create: (_) => ThemeCubit(),
    child: BlocProvider<CounterCubit>(
      create: (_) => CounterCubit(),
      child: const AplikasiSaya(),
    ),
  ),
)

// ✅ Rata dan mudah dibaca
MultiBlocProvider(
  providers: [
    BlocProvider<AuthCubit>(create: (_) => AuthCubit()),
    BlocProvider<ThemeCubit>(create: (_) => ThemeCubit()),
    BlocProvider<CounterCubit>(create: (_) => CounterCubit()),
  ],
  child: const AplikasiSaya(),
)

Menyediakan secara global

Untuk BLoC yang dibutuhkan seluruh aplikasi, taruh di atas MaterialApp:

dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<CounterCubit>(create: (_) => CounterCubit()),
        BlocProvider<InternetCubit>(
          create: (_) => InternetCubit(connectivity: Connectivity()),
        ),
        BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
      ],
      child: MaterialApp(
        title: 'Aplikasi BLoC',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
          useMaterial3: true,
        ),
        home: const HomeScreen(),
      ),
    );
  }
}

Menaruhnya di atas MaterialApp penting: dengan begitu setiap layar yang didorong lewat Navigator tetap bisa mengaksesnya. Alasan teknisnya dibahas di Bab 23.

BlocBuilder

Mendengarkan state dan membangun ulang bagian UI.

dart
BlocBuilder<CounterCubit, int>(
  builder: (context, state) {
    return Text(
      '$state',
      style: Theme.of(context).textTheme.displayMedium,
    );
  },
)

Dua parameter tipenya: jenis BLoC-nya, dan jenis state-nya.

buildWhen: membatasi rebuild

Kadang kamu hanya ingin membangun ulang untuk perubahan tertentu.

dart
BlocBuilder<CounterCubit, CounterState>(
  buildWhen: (sebelum, sesudah) {
    // Hanya bangun ulang kalau nilainya berubah,
    // abaikan perubahan properti lain
    return sebelum.nilai != sesudah.nilai;
  },
  builder: (context, state) => Text('${state.nilai}'),
)

Ini padanan select di Riverpod dan Selector di Provider.

Contoh yang lebih berguna — hanya bereaksi pada transisi tertentu:

dart
BlocBuilder<WeatherCubit, WeatherState>(
  // Jangan tampilkan loading spinner saat menyegarkan,
  // pertahankan data lama agar tidak berkedip
  buildWhen: (sebelum, sesudah) =>
      !(sebelum is WeatherLoaded && sesudah is WeatherLoading),
  builder: (context, state) => switch (state) {
    WeatherLoading() => const Center(child: CircularProgressIndicator()),
    WeatherLoaded(cuaca: final c) => KartuCuaca(cuaca: c),
    WeatherError(pesan: final p) => Center(child: Text(p)),
    WeatherInitial() => const Center(child: Text('Cari kota.')),
  },
)

Menempatkan BlocBuilder sesempit mungkin

Ini praktik yang layak dibiasakan sejak awal.

dart
// ❌ Seluruh Scaffold dibangun ulang setiap kali angka berubah
BlocBuilder<CounterCubit, int>(
  builder: (context, state) {
    return Scaffold(
      appBar: AppBar(title: const Text('Penghitung')),
      body: Center(child: Text('$state')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterCubit>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  },
)

// ✅ Hanya Text yang dibangun ulang
Scaffold(
  appBar: AppBar(title: const Text('Penghitung')),
  body: Center(
    child: BlocBuilder<CounterCubit, int>(
      builder: (context, state) => Text('$state'),
    ),
  ),
  floatingActionButton: FloatingActionButton(
    onPressed: () => context.read<CounterCubit>().increment(),
    child: const Icon(Icons.add),
  ),
)

Prinsipnya sama dengan yang dibahas di Bab 12: batasi cakupan rebuild ke bagian terkecil yang benar-benar berubah.

BlocListener

Untuk hal-hal yang dikerjakan sekali ketika state berubah — bukan digambar.

dart
BlocListener<AuthCubit, AuthState>(
  listener: (context, state) {
    if (state is AuthError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(state.pesan),
          backgroundColor: Theme.of(context).colorScheme.error,
        ),
      );
    }

    if (state is AuthSuccess) {
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (_) => const HomeScreen()),
      );
    }
  },
  child: const FormMasuk(),
)

Perbedaan penting dari BlocBuilder:

BlocBuilderBlocListener
Membangun ulang UIYaTidak
Dipanggil berapa kaliSetiap rebuild bisa sajaSekali per perubahan state
UntukMenggambarEfek samping

Jangan tampilkan snackbar di dalam builder

Ini kesalahan yang sering terjadi dan akibatnya aneh:

dart
// ❌ Snackbar bisa muncul berkali-kali
BlocBuilder<AuthCubit, AuthState>(
  builder: (context, state) {
    if (state is AuthError) {
      ScaffoldMessenger.of(context).showSnackBar(...);   // 💥
    }
    return const FormMasuk();
  },
)

builder bisa dipanggil beberapa kali untuk state yang sama — misalnya ketika induknya dibangun ulang, atau saat orientasi berubah. Efek samping di dalamnya akan berulang. listener dijamin dipanggil tepat sekali per perubahan state.

listenWhen

Sama seperti buildWhen, tetapi untuk listener:

dart
BlocListener<CounterCubit, CounterState>(
  listenWhen: (sebelum, sesudah) => sesudah.nilai < 0,
  listener: (context, state) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Nilai tidak boleh negatif!')),
    );
  },
  child: const HalamanPenghitung(),
)

MultiBlocListener

dart
MultiBlocListener(
  listeners: [
    BlocListener<AuthCubit, AuthState>(
      listener: (context, state) { /* ... */ },
    ),
    BlocListener<InternetCubit, InternetState>(
      listener: (context, state) { /* ... */ },
    ),
  ],
  child: const HomeScreen(),
)

BlocConsumer

Ketika kamu butuh membangun dan bereaksi di tempat yang sama:

dart
BlocConsumer<CounterCubit, CounterState>(
  listenWhen: (sebelum, sesudah) => sesudah.nilai % 10 == 0,
  listener: (context, state) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Kelipatan sepuluh: ${state.nilai}')),
    );
  },
  buildWhen: (sebelum, sesudah) => sebelum.nilai != sesudah.nilai,
  builder: (context, state) {
    return Text(
      '${state.nilai}',
      style: Theme.of(context).textTheme.displayMedium,
    );
  },
)

Kalau kamu tidak butuh keduanya, jangan pakai BlocConsumer — pilih salah satu yang lebih spesifik. Kode yang menyatakan maksudnya dengan tepat lebih mudah dibaca.

Mengakses BLoC lewat context

Ada tiga cara, dan sekali lagi memilih yang tepat itu penting.

dart
// 1. read — ambil instance tanpa berlangganan
//    Untuk memanggil metode di dalam callback
context.read<CounterCubit>().increment();

// 2. watch — berlangganan, widget dibangun ulang saat berubah
//    Hanya boleh di dalam build()
final state = context.watch<CounterCubit>().state;

// 3. select — berlangganan satu bagian saja
final nilai = context.select<CounterCubit, int>((c) => c.state.nilai);

watch versus BlocBuilder

context.watch membangun ulang seluruh widget tempat ia dipanggil. BlocBuilder hanya membangun ulang isi builder-nya.

dart
// ❌ Seluruh HalamanBerat dibangun ulang
class HalamanBerat extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final nilai = context.watch<CounterCubit>().state;
    return Column(
      children: [
        const KontenSangatBerat(),
        Text('$nilai'),
      ],
    );
  }
}

// ✅ Hanya Text yang dibangun ulang
class HalamanBerat extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const KontenSangatBerat(),
        BlocBuilder<CounterCubit, int>(
          builder: (ctx, nilai) => Text('$nilai'),
        ),
      ],
    );
  }
}

Untuk widget kecil, context.watch nyaman. Untuk widget besar, BlocBuilder hampir selalu pilihan yang lebih baik.

BuildContext: kenapa ia penting di sini

Ini bagian yang layak dipahami dengan benar, karena kesalahpahaman tentang BuildContext adalah sumber error BLoC yang paling membingungkan.

BuildContext adalah penunjuk ke posisi sebuah widget di dalam element tree — pohon yang kita bahas di Bab 12.

Ketika kamu memanggil context.read<CounterCubit>(), Flutter menelusuri element tree ke atas dari posisi context itu, mencari BlocProvider<CounterCubit> terdekat.

   MaterialApp

   BlocProvider<CounterCubit>   ◄────────┐
       │                                 │ ditemukan di sini
   Scaffold                              │
       │                                 │
   Column                                │ pencarian ke atas
       │                                 │
   BlocBuilder ──────── context.read() ──┘

Kalau tidak ditemukan, kamu mendapat error:

Error: Could not find the correct Provider<CounterCubit> above this
HomeScreen Widget.

Jebakan context yang paling umum

Kesalahan klasik: memakai context yang berada di atas BlocProvider.

dart
// ❌ SALAH
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<CounterCubit>(
      create: (_) => CounterCubit(),
      child: Scaffold(
        body: ElevatedButton(
          // `context` di sini adalah context MILIK MyApp,
          // yang berada DI ATAS BlocProvider.
          // Pencarian ke atas tidak akan menemukan CounterCubit.
          onPressed: () => context.read<CounterCubit>().increment(),
          child: const Text('Tambah'),
        ),
      ),
    );
  }
}

Ada dua cara memperbaikinya.

Cara 1 — pecah menjadi widget terpisah (paling disarankan):

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

  @override
  Widget build(BuildContext context) {
    return BlocProvider<CounterCubit>(
      create: (_) => CounterCubit(),
      child: const HomeScreen(),   // context-nya berada DI BAWAH provider
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ElevatedButton(
        onPressed: () => context.read<CounterCubit>().increment(),
        child: const Text('Tambah'),
      ),
    );
  }
}

Cara 2 — pakai Builder untuk membuat context baru di bawah provider:

dart
BlocProvider<CounterCubit>(
  create: (_) => CounterCubit(),
  child: Builder(
    builder: (context) {   // context BARU, di bawah BlocProvider
      return ElevatedButton(
        onPressed: () => context.read<CounterCubit>().increment(),
        child: const Text('Tambah'),
      );
    },
  ),
)

Cara pertama lebih disukai karena sekaligus memperbaiki struktur kode. Ini juga masalah yang sama dengan Scaffold.of(context) yang sering ditemui pemula.

Contoh lengkap: penghitung dengan pemantau koneksi

Menggabungkan semuanya menjadi aplikasi yang memakai tiga BLoC sekaligus.

dart
// lib/logic/cubit/internet_cubit.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:equatable/equatable.dart';

sealed class InternetState extends Equatable {
  const InternetState();

  @override
  List<Object?> get props => [];
}

final class InternetLoading extends InternetState {
  const InternetLoading();
}

final class InternetConnected extends InternetState {
  const InternetConnected(this.jenis);
  final JenisKoneksi jenis;

  @override
  List<Object?> get props => [jenis];
}

final class InternetDisconnected extends InternetState {
  const InternetDisconnected();
}

enum JenisKoneksi { wifi, seluler }

class InternetCubit extends Cubit<InternetState> {
  InternetCubit({required this.connectivity}) : super(const InternetLoading()) {
    pantauKoneksi();
  }

  final Connectivity connectivity;
  StreamSubscription<List<ConnectivityResult>>? _langganan;

  void pantauKoneksi() {
    _langganan = connectivity.onConnectivityChanged.listen((hasil) {
      if (hasil.contains(ConnectivityResult.wifi)) {
        emitConnected(JenisKoneksi.wifi);
      } else if (hasil.contains(ConnectivityResult.mobile)) {
        emitConnected(JenisKoneksi.seluler);
      } else {
        emitDisconnected();
      }
    });
  }

  void emitConnected(JenisKoneksi jenis) => emit(InternetConnected(jenis));

  void emitDisconnected() => emit(const InternetDisconnected());

  @override
  Future<void> close() {
    _langganan?.cancel();   // ⚠️ wajib
    return super.close();
  }
}

Perhatikan close() yang di-override untuk membatalkan langganan stream. Ini padanan dispose() pada StatefulWidget — dan sama wajibnya.

Layar yang memakai keduanya:

dart
// lib/presentation/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../logic/cubit/counter_cubit.dart';
import '../../logic/cubit/internet_cubit.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Beranda')),

      // Listener di level halaman untuk efek samping
      body: MultiBlocListener(
        listeners: [
          BlocListener<InternetCubit, InternetState>(
            listener: (context, state) {
              if (state is InternetDisconnected) {
                ScaffoldMessenger.of(context)
                  ..clearSnackBars()
                  ..showSnackBar(
                    const SnackBar(
                      content: Text('Koneksi internet terputus.'),
                      backgroundColor: Colors.redAccent,
                      duration: Duration(days: 1),   // tetap sampai tersambung
                    ),
                  );
              } else if (state is InternetConnected) {
                ScaffoldMessenger.of(context).clearSnackBars();
              }
            },
          ),
          BlocListener<CounterCubit, int>(
            listenWhen: (sebelum, sesudah) => sesudah < 0,
            listener: (context, state) {
              showDialog(
                context: context,
                builder: (ctx) => AlertDialog(
                  title: const Text('Nilai Negatif'),
                  content: const Text('Penghitung tidak boleh di bawah nol.'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(ctx),
                      child: const Text('Mengerti'),
                    ),
                  ],
                ),
              );
            },
          ),
        ],
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // Indikator koneksi
              BlocBuilder<InternetCubit, InternetState>(
                builder: (context, state) {
                  return switch (state) {
                    InternetLoading() => const Text('Memeriksa koneksi...'),
                    InternetConnected(jenis: final j) => Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(
                            j == JenisKoneksi.wifi
                                ? Icons.wifi
                                : Icons.signal_cellular_alt,
                            color: Colors.green,
                          ),
                          const SizedBox(width: 8),
                          Text(
                            j == JenisKoneksi.wifi ? 'Wi-Fi' : 'Data Seluler',
                            style: const TextStyle(color: Colors.green),
                          ),
                        ],
                      ),
                    InternetDisconnected() => const Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(Icons.wifi_off, color: Colors.red),
                          SizedBox(width: 8),
                          Text('Terputus', style: TextStyle(color: Colors.red)),
                        ],
                      ),
                  };
                },
              ),

              const SizedBox(height: 40),
              const Text('Kamu telah menekan tombol sebanyak:'),

              // Hanya angka ini yang dibangun ulang
              BlocBuilder<CounterCubit, int>(
                builder: (context, state) => Text(
                  '$state',
                  style: Theme.of(context).textTheme.displayMedium,
                ),
              ),

              const SizedBox(height: 24),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  FloatingActionButton(
                    heroTag: 'kurang',
                    onPressed: () =>
                        context.read<CounterCubit>().decrement(),
                    child: const Icon(Icons.remove),
                  ),
                  const SizedBox(width: 24),
                  FloatingActionButton(
                    heroTag: 'tambah',
                    onPressed: () =>
                        context.read<CounterCubit>().increment(),
                    child: const Icon(Icons.add),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Alurnya kalau digambarkan:

RepositoryProvider

Selain BLoC, kamu sering perlu menyediakan repository atau layanan. Untuk itu ada RepositoryProvider:

dart
void main() {
  runApp(
    MultiRepositoryProvider(
      providers: [
        RepositoryProvider<WeatherRepository>(
          create: (_) => WeatherRepository(WeatherApi()),
        ),
        RepositoryProvider<AuthRepository>(
          create: (_) => AuthRepository(),
        ),
      ],
      child: const AplikasiSaya(),
    ),
  );
}

Dan BLoC mengambilnya dari sana:

dart
BlocProvider<WeatherCubit>(
  create: (context) => WeatherCubit(
    context.read<WeatherRepository>(),
  ),
  child: const WeatherScreen(),
)

Perbedaannya dari BlocProvider: RepositoryProvider tidak memanggil close(), karena repository bukan stream. Ia hanya menyediakan objek.

Pola ini menjadi fondasi arsitektur berlapis di Bab 22.

Latihan Mandiri

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

Variasi 1: Layar Login Empat Widget — ⭐⭐ · 40–60 menit

Tantangan: Buat layar login yang memakai keempat widget dari bab ini sekaligus, masing-masing pada tempatnya yang benar: BlocProvider menyediakan, BlocBuilder menggambar keadaan tombol, BlocListener menampilkan snackbar kesalahan dan berpindah layar saat berhasil, dan BlocConsumer di satu tempat yang memang membutuhkan keduanya.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│  Email    [_______________]        │
│  Sandi    [_______________]        │
│                                     │
│           [ Masuk ]                │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Hanya tombol "Masuk" itu (bukan seluruh layar) yang dibungkus BlocBuilder. Gagal masuk → BlocListener menampilkan snackbar; berhasil → BlocListener berpindah ke Beranda. Keduanya terjadi di listener, tidak pernah di builder.

Kriteria selesai:

  • Tidak ada satu pun snackbar, dialog, atau navigasi yang dipanggil dari dalam builder.
  • BlocBuilder membungkus sesempit mungkin — hanya tombol dan indikator loading-nya, bukan seluruh Scaffold.
  • buildWhen dipakai setidaknya sekali untuk mencegah rebuild yang tidak perlu, dan kamu bisa menunjukkan rebuild mana yang dicegahnya.
  • Kamu sengaja membuat error "tidak menemukan BLoC" dengan memakai context yang berada di atas BlocProvider, lalu memperbaikinya dengan Builder atau widget terpisah.

Petunjuk: Kriteria terakhir adalah jebakan paling umum di seluruh Bagian III, dan jauh lebih baik kamu temui sekarang dengan sengaja daripada nanti pada jam dua pagi. Penyebabnya selalu sama: pencarian BLoC berjalan ke atas dari posisi context di element tree, jadi context yang berasal dari build milik widget yang memasang BlocProvider berada di atas provider itu, bukan di bawahnya. Soal snackbar: builder bisa dipanggil berkali-kali untuk state yang sama — Flutter berhak membangun ulang kapan pun ia mau. listener dijamin berjalan tepat sekali per perubahan state, dan itulah kenapa efek samping harus tinggal di sana.

Variasi 2: Pemesanan Lapangan — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun layar pemesanan lapangan futsal yang menampilkan kisi jam tersedia, jam yang dipilih, dan ringkasan harga. Ukur berapa kali tiap bagian dibangun ulang, lalu pakai buildWhen dan listenWhen untuk menekannya seminimal mungkin.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│  08:00  09:00  10:00  11:00        │
│  [ ○ ]  [ ● ]  [ ✗ ]  [ ○ ]        │
│  12:00  13:00  14:00  15:00        │
│  [ ○ ]  [ ○ ]  [ ✗ ]  [ ○ ]        │
│                                     │
│  Jam dipilih: 09:00                │
│  Total: Rp 150.000                 │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Tiap sel jam ([ ○ ]) dibungkus BlocBuilder-nya sendiri — memilih satu sel tidak membangun ulang tujuh sel lain. Kalau ternyata jam itu sudah dipesan orang lain, snackbar muncul sekali saja (listenWhen), bukan berulang untuk state yang sama.

Kriteria selesai:

  • Kamu mencatat jumlah rebuild sebelum dan sesudah optimasi dengan debugPrint di tiap builder.
  • Memilih satu jam tidak membangun ulang seluruh kisi jam.
  • listenWhen mencegah snackbar "jam sudah dipesan orang lain" muncul berulang untuk state yang sama.
  • Ringkasan harga memakai context.select, bukan BlocBuilder penuh.

Petunjuk: buildWhen menerima state lama dan state baru, sehingga kamu bisa memutuskan berdasarkan apa yang berubah, bukan hanya berdasarkan state sekarang. Untuk kisi jam, pola yang paling efektif adalah membungkus tiap sel dengan BlocBuilder sendiri yang buildWhen-nya hanya memeriksa apakah status jam itu yang berubah — terdengar berlebihan, tetapi itulah bedanya membangun ulang satu sel versus empat puluh delapan sel.

Variasi 3: Aplikasi Laundry Berlapis — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun kerangka aplikasi laundry dengan tiga BLoC — pesanan, tarif, dan notifikasi — plus satu repository yang dipakai bersama. Susun penyediaannya dengan benar sehingga seluruh layar yang didorong Navigator tetap bisa mengaksesnya.

Bayangan susunan provider:

text
RepositoryProvider (Repository — dibuat SEKALI)
   └─ MultiBlocProvider (Pesanan, Tarif, Notifikasi)
        └─ MaterialApp
             └─ Navigator.push(LayarBaru)
                  → LayarBaru TETAP bisa akses ketiga BLoC,
                    tanpa provider baru diberikan ulang

Kriteria selesai:

  • RepositoryProvider berada di atas MultiBlocProvider, yang berada di atas MaterialApp.
  • Layar yang didorong lewat Navigator bisa mengakses ketiga BLoC tanpa diberikan ulang.
  • Satu BLoC yang harus langsung aktif dibuat dengan lazy: false, dan kamu bisa menjelaskan kenapa yang lain tidak perlu.
  • Repository dibuat sekali, dan ketiga BLoC menerima instance yang sama.

Petunjuk: Urutan penyediaan mengikuti arah ketergantungan: repository tidak bergantung pada apa pun, BLoC bergantung pada repository, dan MaterialApp membutuhkan keduanya. Menaruh MultiBlocProvider di bawah MaterialApp adalah kesalahan yang gejalanya baru muncul saat kamu mendorong layar kedua — dan pada saat itu penyebabnya sudah terasa jauh dari tempat errornya.

Variasi 4: Cakupan Dialog dan Bottom Sheet — ⭐⭐⭐ · 45–60 menit

Tantangan: Buat layar yang membuka dialog dan bottom sheet yang keduanya perlu membaca dan mengubah BLoC milik layar itu. Buat dulu versi yang gagal, amati errornya, lalu perbaiki dengan cara yang benar.

Bayangan alur:

text
[Layar Induk + BLoC]

   ├─ versi GAGAL: showDialog(...) langsung
   │    → dialog TIDAK bisa baca BLoC layar induk
   │    → error "BlocProvider.of() called with a context that
   │       does not contain a Bloc"

   └─ versi BENAR: showDialog(
         builder: (_) => BlocProvider.value(
           value: context.read<Bloc>(),  ← context dari LAYAR INDUK
           child: DialogKonten(),
         ))
        → perubahan dari dalam dialog terlihat di layar induk
          setelah dialog ditutup

Kriteria selesai:

  • Versi gagal dibuat lebih dulu, dan kamu mencatat bunyi errornya.
  • Versi benar bekerja untuk dialog maupun bottom sheet.
  • Perubahan yang dilakukan dari dalam dialog terlihat di layar induk setelah dialog ditutup.
  • Kamu bisa menjelaskan kenapa dialog tidak otomatis mewarisi provider layar yang membukanya.

Petunjuk: Dialog dan bottom sheet punya masalah yang persis sama dengan Navigator.push yang dibahas di Bab 23 — keduanya dipasang sebagai rute baru di dekat akar pohon, bukan sebagai anak layar yang membukanya. BlocProvider.value memasang ulang instance yang sama di cabang baru itu. Perhatikan bahwa context yang kamu pakai untuk membaca BLoC harus berasal dari layar asal, bukan dari builder dialognya.

Variasi 5: Formulir Donor Darah — ⭐⭐⭐⭐ · 60–90 menit

Tantangan: Bangun formulir pendaftaran donor darah berisi delapan kolom dengan validasi per kolom, validasi silang (tanggal donor terakhir versus kelayakan), dan kirim yang bisa gagal. Kelola seluruhnya dengan satu BLoC, dan pilih widget penghubung yang tepat untuk tiap bagian.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│  Nama        [_______________]     │
│  Berat (kg)  [___]                 │
│  ⚠ Minimal 45kg                    │
│  Gol. Darah  ( O ▾ )               │
│  Tgl Donor Terakhir  [_____▾]      │
│  ⚠ Belum cukup 3 bulan sejak donor │
│           [ Kirim ] (nonaktif)     │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Kolom "Berat" hanya rebuild ketika pesan error miliknya sendiri berubah — mengetik di kolom "Nama" tidak menyentuhnya sama sekali.

Kriteria selesai:

  • Setiap kolom hanya dibangun ulang ketika pesan kesalahannya sendiri berubah.
  • Tombol kirim nonaktif selama formulir belum valid dan selama pengiriman berlangsung.
  • Kegagalan pengiriman menampilkan pesan lewat listener dan tidak mengosongkan isian yang sudah diketik.
  • BlocConsumer dipakai hanya di tempat yang benar-benar butuh menggambar dan bereaksi sekaligus, bukan di mana-mana.

Petunjuk: Godaan terbesar di formulir berbasis BLoC adalah membungkus seluruh formulir dengan satu BlocBuilder — hasilnya delapan kolom dibangun ulang setiap kali satu huruf diketik, dan kursor bisa berperilaku aneh. Bungkus per kolom dengan buildWhen yang memeriksa pesan kesalahan kolom itu saja. Dan BlocConsumer sebenarnya hanya gabungan BlocBuilder dan BlocListener; memakainya di tempat yang hanya butuh salah satu justru mengaburkan maksud kodemu.

Ikhtisar

  • BlocProvider membuat dan menyediakan instance; ia memanggil close() otomatis saat dilepas. Pakai lazy: false kalau harus langsung aktif.
  • MultiBlocProvider meratakan penyediaan beberapa BLoC.
  • Taruh BLoC global di atas MaterialApp agar layar yang didorong lewat Navigator tetap bisa mengaksesnya.
  • BlocBuilder untuk menggambar; BlocListener untuk efek samping; BlocConsumer kalau butuh keduanya di tempat yang sama.
  • Jangan tampilkan snackbar atau navigasi di dalam builderbuilder bisa dipanggil berkali-kali untuk state yang sama. listener dijamin sekali.
  • buildWhen dan listenWhen membatasi kapan keduanya dijalankan.
  • Tempatkan BlocBuilder sesempit mungkin, bukan membungkus seluruh Scaffold.
  • context.read di callback, context.watch di build, context.select untuk satu bagian. context.watch membangun ulang seluruh widget — BlocBuilder lebih presisi.
  • BuildContext menunjuk posisi di element tree. Pencarian BLoC berjalan ke atas dari posisi itu.
  • Jebakan paling umum: memakai context yang berada di atasBlocProvider. Perbaiki dengan memecah ke widget terpisah, atau bungkus dengan Builder.
  • Override close() pada Cubit untuk membatalkan StreamSubscription — padanan dispose().
  • RepositoryProvider untuk layanan dan repository; ia tidak memanggil close().

Berikutnya: Bab 22 — Arsitektur Berlapis dengan BLoC.

Transkrip asli

Disintesis dari 2_flutter_bloc-zero-to-hero/3_flutter-bloc-concepts.md (BlocProvider, BlocBuilder, BlocListener, BlocConsumer, MultiBlocProvider) dan 8_build-context-in-depth.md. Lihat PDF BLoC Zero to Hero.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.