Tampilan
Bab 20 — BLoC: Konsep Inti
BLoC adalah pendekatan yang berbeda secara mendasar dari Riverpod dan Provider. Ia lebih ketat, lebih banyak kode, dan lebih sulit di awal — tetapi memberi struktur yang sangat konsisten untuk aplikasi besar, dan riwayat perubahan yang bisa dilacak sepenuhnya.
Bab ini adalah fondasi seluruh rangkaian BLoC. Bacalah pelan-pelan. Kalau konsep di bab ini belum mantap, enam bab berikutnya akan terasa membingungkan.
Stream: fondasi dari segalanya
Sebelum bicara BLoC, kamu harus benar-benar paham apa itu stream. Mari pakai analogi yang dipakai di kursus sumbernya, karena analogi itu bagus.
Bayangkan kamu dan seorang teman berada di daerah pegunungan. Ada sungai yang mengalir dari atas ke bawah. Kamu naik ke hulu, temanmu menunggu di hilir. Sungai itu berkelok-kelok sehingga kalian tidak bisa saling melihat.
Tugasnya: kamu mengirimkan sepuluh perahu kecil ke hilir, masing-masing berisi angka sesuai urutan pengirimannya. Perahu pertama berisi angka satu, perahu kedua berisi angka dua, dan seterusnya. Temanmu di hilir menyebutkan angka setiap kali sebuah perahu tiba.
HULU (pengirim)
│
🚣 1 ──┐
│
🚣 2 ──┤ Sungai berkelok
│ (asinkron — tidak tahu
🚣 3 ──┤ kapan tiba)
│
▼ │
HILIR (penerima)
"satu!" ... "dua!" ... "tiga!"Itulah stream. Aliran data yang dikirim dari satu sisi dan didengarkan di sisi lain, di mana penerima tidak tahu kapan persisnya data akan tiba. Ia hanya menunggu — atau lebih tepatnya, mendengarkan.
Stream dalam kode Dart
dart
Stream<int> alirkanPerahu() async* {
for (var i = 1; i <= 10; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i; // kirim satu perahu
}
}Ada tiga kata kunci baru di sini:
async* (dengan bintang) menandai generator asinkron — fungsi yang menghasilkan banyak nilai dari waktu ke waktu, bukan satu nilai seperti async biasa.
yield mengirimkan satu nilai ke dalam stream, lalu melanjutkan eksekusi. Bandingkan dengan return yang mengakhiri fungsi.
await di dalamnya tetap bekerja seperti biasa — menunggu sebelum melanjutkan.
Mendengarkannya:
dart
void main() async {
// Cara 1: await for
await for (final angka in alirkanPerahu()) {
print('Perahu tiba: $angka');
}
// Cara 2: listen
alirkanPerahu().listen(
(angka) => print('Perahu tiba: $angka'),
onError: (kesalahan) => print('Perahu tenggelam: $kesalahan'),
onDone: () => print('Semua perahu sudah tiba'),
);
}Perbedaan Future dan Stream bisa diringkas begini:
Future | Stream | |
|---|---|---|
| Jumlah nilai | Satu | Banyak |
| Analogi | Satu paket kiriman | Langganan majalah |
| Menunggu dengan | await | await for atau .listen() |
| Menghasilkan dengan | async + return | async* + yield |
Kenapa ini penting? Karena BLoC pada dasarnya adalah stream. Sebuah BLoC menerima aliran masukan dan menghasilkan aliran keluaran. Semua yang lain hanya pembungkus di atas ide itu.
Memasang
yaml
# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.6
equatable: ^2.0.7flutter_bloc sudah menyertakan paket bloc inti. equatable akan kita bahas di bagian akhir bab ini — dan ia lebih penting daripada kelihatannya.
Cubit: versi sederhana
BLoC punya dua bentuk. Kita mulai dari yang lebih sederhana: Cubit.
Cubit adalah kelas yang menyimpan sebuah state dan menyediakan metode untuk mengubahnya.
dart
// lib/logic/cubit/counter_cubit.dart
import 'package:bloc/bloc.dart';
class CounterCubit extends Cubit<int> {
// Nilai awal dioper ke konstruktor super
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
}Itu seluruhnya. Tiga hal yang perlu diperhatikan:
Cubit<int> — parameter tipe menyatakan bentuk state-nya. Di sini sebuah bilangan bulat.
super(0) — state awal.
emit(...) — satu-satunya cara mengubah state. Setiap emit mengirimkan state baru ke dalam stream, dan semua yang mendengarkan akan menerimanya.
CounterCubit
┌─────────────────────────────────────────┐
│ state: 0 │
│ │
│ increment() ──► emit(1) ──┐ │
│ increment() ──► emit(2) ──┤ │
│ decrement() ──► emit(1) ──┤ stream │
│ reset() ──► emit(0) ──┤ of states │
└────────────────────────────┼────────────┘
▼
0 → 1 → 2 → 1 → 0
│
▼
UI diperbaruiemit mengabaikan state yang sama
Kalau kamu memanggil emit(0) sementara state sudah bernilai 0, tidak ada yang terjadi. BLoC membandingkan state baru dengan yang lama, dan kalau sama, ia tidak mengirimkan apa pun.
Ini fitur, bukan bug — ia mencegah rebuild yang tidak perlu. Tetapi ia menjadi sumber kebingungan besar ketika state-mu adalah objek kustom. Bagian Equatable di bawah menjelaskannya.
Menghubungkan Cubit ke UI
Ada dua widget yang kamu butuhkan: BlocProvider untuk menyediakan cubit, dan BlocBuilder untuk mendengarkannya.
dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'logic/cubit/counter_cubit.dart';
import 'presentation/screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<CounterCubit>(
create: (context) => CounterCubit(),
child: MaterialApp(
title: 'BLoC Counter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const HomeScreen(title: 'Beranda', color: Colors.blueAccent),
),
);
}
}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';
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
required this.title,
required this.color,
});
final String title;
final Color color;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: color,
appBar: AppBar(title: Text(title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Kamu telah menekan tombol sebanyak:'),
// BlocBuilder mendengarkan dan membangun ulang saat state berubah
BlocBuilder<CounterCubit, int>(
builder: (context, state) {
return Text(
'$state',
style: Theme.of(context).textTheme.displayMedium,
);
},
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
heroTag: 'kurang',
onPressed: () {
// context.read mengambil cubit tanpa berlangganan
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),
),
],
),
],
),
),
);
}
}Perhatikan heroTag pada kedua FloatingActionButton. Dua FAB dalam satu layar akan menyebabkan error tag Hero duplikat kalau tidak dibedakan — jebakan kecil yang sering muncul.
Bloc: versi berbasis event
Cubit memanggil metode secara langsung. Bloc menambahkan satu lapisan: alih-alih memanggil metode, UI mengirimkan event, dan BLoC memutuskan apa yang harus dilakukan terhadapnya.
CUBIT BLOC
───────────────────── ─────────────────────────────────
UI memanggil metode UI mengirim event
│ │
cubit.increment() bloc.add(IncrementEvent())
│ │
▼ ▼
emit(state + 1) Event masuk antrean
│
▼
Handler memprosesnya
│
▼
emit(state baru)
✅ Sederhana ✅ Setiap perubahan tercatat
✅ Sedikit kode ✅ Bisa ditelusuri & diuji
❌ Tidak ada jejak event ❌ Lebih banyak kodeMendefinisikan event
dart
// lib/logic/bloc/counter_event.dart
part of 'counter_bloc.dart';
sealed class CounterEvent {}
final class IncrementCounter extends CounterEvent {}
final class DecrementCounter extends CounterEvent {}
final class ResetCounter extends CounterEvent {}
final class SetCounter extends CounterEvent {
SetCounter(this.nilai);
final int nilai;
}Kata kunci sealed sangat berguna di sini. Ia memberi tahu compiler bahwa seluruh subkelas CounterEvent berada di berkas ini — sehingga switch pada event bisa diperiksa kelengkapannya oleh compiler.
Mendefinisikan BLoC
dart
// lib/logic/bloc/counter_bloc.dart
import 'package:bloc/bloc.dart';
part 'counter_event.dart';
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
// Daftarkan handler untuk setiap jenis event
on<IncrementCounter>((event, emit) {
emit(state + 1);
});
on<DecrementCounter>((event, emit) {
emit(state - 1);
});
on<ResetCounter>((event, emit) {
emit(0);
});
on<SetCounter>((event, emit) {
emit(event.nilai);
});
}
}Bloc<Event, State> punya dua parameter tipe: jenis event yang diterima, dan bentuk state yang dihasilkan.
Metode on<T>() mendaftarkan handler untuk satu jenis event. Ia menerima dua argumen: event (instance event-nya, berguna kalau event membawa data) dan emit (fungsi untuk mengeluarkan state baru).
Memakainya di UI
dart
BlocBuilder<CounterBloc, int>(
builder: (context, state) => Text('$state'),
)
// Mengirim event
context.read<CounterBloc>().add(IncrementCounter());
context.read<CounterBloc>().add(SetCounter(100));State kustom
State berupa int cukup untuk penghitung, tetapi aplikasi nyata butuh state yang lebih kaya — biasanya mengandung data, status pemuatan, dan pesan kesalahan.
Ada dua gaya yang umum dipakai.
Gaya 1: satu kelas dengan status
dart
// lib/logic/cubit/weather_state.dart
import 'package:equatable/equatable.dart';
import '../../data/models/weather.dart';
enum StatusCuaca { awal, memuat, berhasil, gagal }
class WeatherState extends Equatable {
const WeatherState({
this.status = StatusCuaca.awal,
this.cuaca,
this.pesanKesalahan,
});
final StatusCuaca status;
final Weather? cuaca;
final String? pesanKesalahan;
WeatherState copyWith({
StatusCuaca? status,
Weather? cuaca,
String? pesanKesalahan,
}) {
return WeatherState(
status: status ?? this.status,
cuaca: cuaca ?? this.cuaca,
pesanKesalahan: pesanKesalahan,
);
}
@override
List<Object?> get props => [status, cuaca, pesanKesalahan];
}Gaya 2: sealed class per keadaan
dart
sealed class WeatherState extends Equatable {
const WeatherState();
@override
List<Object?> get props => [];
}
final class WeatherInitial extends WeatherState {
const WeatherInitial();
}
final class WeatherLoading extends WeatherState {
const WeatherLoading();
}
final class WeatherLoaded extends WeatherState {
const WeatherLoaded(this.cuaca);
final Weather cuaca;
@override
List<Object?> get props => [cuaca];
}
final class WeatherError extends WeatherState {
const WeatherError(this.pesan);
final String pesan;
@override
List<Object?> get props => [pesan];
}Gaya kedua lebih disukai di proyek besar, karena switch di UI menjadi eksplisit dan compiler memastikan kamu menangani semua kemungkinan:
dart
BlocBuilder<WeatherCubit, WeatherState>(
builder: (context, state) {
return switch (state) {
WeatherInitial() => const Center(
child: Text('Cari kota untuk melihat cuacanya.'),
),
WeatherLoading() => const Center(
child: CircularProgressIndicator(),
),
WeatherLoaded(cuaca: final c) => KartuCuaca(cuaca: c),
WeatherError(pesan: final p) => Center(child: Text(p)),
};
},
)Karena WeatherState bertanda sealed, compiler tahu hanya ada empat kemungkinan. Kalau kamu menambah keadaan kelima nanti, compiler akan mengingatkanmu bahwa switch ini belum lengkap. Perlindungan gratis.
Cubit dengan state kustom
dart
// lib/logic/cubit/weather_cubit.dart
import 'package:bloc/bloc.dart';
import '../../data/repositories/weather_repository.dart';
import 'weather_state.dart';
class WeatherCubit extends Cubit<WeatherState> {
WeatherCubit(this._repository) : super(const WeatherInitial());
final WeatherRepository _repository;
Future<void> ambilCuaca(String kota) async {
emit(const WeatherLoading());
try {
final cuaca = await _repository.ambilCuaca(kota);
emit(WeatherLoaded(cuaca));
} catch (kesalahan) {
emit(WeatherError('Gagal memuat cuaca untuk $kota.'));
}
}
void reset() => emit(const WeatherInitial());
}Pola emit(Loading) → operasi → emit(Loaded) atau emit(Error) adalah pola paling umum di seluruh BLoC. Kamu akan menulisnya berkali-kali.
Equatable dan masalah state tidak berubah
Ini bagian yang paling sering menjebak pendatang baru di BLoC, jadi mari dibahas dengan teliti.
Ingat bahwa emit mengabaikan state yang sama dengan sebelumnya. Pertanyaannya: bagaimana Dart memutuskan dua objek itu "sama"?
Secara bawaan, Dart membandingkan identitas — apakah keduanya objek yang persis sama di memori. Bukan isinya.
dart
class Pengguna {
Pengguna(this.nama, this.umur);
final String nama;
final int umur;
}
final a = Pengguna('Firnanda', 25);
final b = Pengguna('Firnanda', 25);
print(a == b); // false! Isinya sama, tapi objeknya berbedaIni menyebabkan dua masalah yang berlawanan.
Masalah 1: UI dibangun ulang padahal tidak perlu
dart
// Tanpa Equatable
class WeatherState {
WeatherState(this.suhu);
final int suhu;
}
emit(WeatherState(25));
emit(WeatherState(25)); // objek baru, dianggap berbeda
// → UI dibangun ulang percumaMasalah 2: UI tidak diperbarui padahal seharusnya
Ini yang lebih berbahaya:
dart
// ❌ Mengubah list yang sama
void tambahTugas(Tugas t) {
state.daftar.add(t); // objek yang SAMA
emit(state); // BLoC: "ini sama saja" → diabaikan
}State-nya secara logis berubah, tetapi objeknya sama, jadi emit tidak melakukan apa-apa. UI diam, dan tidak ada error apa pun untuk membantumu.
Solusinya: Equatable
Paket equatable mengubah perbandingan dari identitas menjadi isi:
dart
import 'package:equatable/equatable.dart';
class Pengguna extends Equatable {
const Pengguna(this.nama, this.umur);
final String nama;
final int umur;
@override
List<Object?> get props => [nama, umur]; // properti yang dibandingkan
}
final a = Pengguna('Firnanda', 25);
final b = Pengguna('Firnanda', 25);
print(a == b); // true! ✅Getter props mendaftar properti mana yang menentukan kesamaan. Kalau semuanya sama, kedua objek dianggap sama.
Jangan lupa memasukkan properti ke props
Properti yang tidak ada di props tidak ikut dibandingkan — sehingga perubahan padanya tidak akan terdeteksi.
dart
class WeatherState extends Equatable {
const WeatherState({required this.suhu, required this.kota});
final int suhu;
final String kota;
@override
List<Object?> get props => [suhu]; // ❌ kota terlupa!
}
// Perubahan kota tidak akan pernah memicu rebuild
emit(WeatherState(suhu: 25, kota: 'Jakarta'));
emit(WeatherState(suhu: 25, kota: 'Bandung')); // diabaikan!Aturan praktis: masukkan semua properti ke props, kecuali kamu punya alasan sangat spesifik untuk tidak melakukannya.
Dan tetap ingat aturan immutability:
dart
// ✅ Selalu buat objek baru
emit(state.copyWith(daftar: [...state.daftar, tugasBaru]));
// ✅ Untuk list
emit(state.copyWith(
daftar: state.daftar.where((t) => t.id != id).toList(),
));
// ❌ Tidak pernah mengubah objek yang ada
state.daftar.add(tugasBaru);
emit(state);Struktur folder untuk proyek BLoC
Konvensi yang banyak dipakai di komunitas BLoC:
lib/
├── main.dart
├── data/
│ ├── models/ ← kelas data murni
│ │ └── weather.dart
│ ├── data_providers/ ← sumber data mentah (API, DB)
│ │ └── weather_api.dart
│ └── repositories/ ← menggabungkan & menyiapkan data
│ └── weather_repository.dart
├── logic/
│ ├── cubit/
│ │ ├── counter_cubit.dart
│ │ ├── weather_cubit.dart
│ │ └── weather_state.dart
│ └── bloc/
│ ├── counter_bloc.dart
│ └── counter_event.dart
└── presentation/
├── screens/
│ ├── home_screen.dart
│ └── second_screen.dart
├── widgets/
│ └── weather_card.dart
└── router/
└── app_router.dartPemisahan data / logic / presentation inilah yang akan kita bahas tuntas di Bab 22.
Kapan Cubit, kapan Bloc
Pertanyaan yang wajar, dan jawabannya cukup praktis.
Pakai Cubit kalau:
- Perubahan state dipicu langsung oleh aksi sederhana.
- Kamu tidak butuh riwayat "apa yang menyebabkan perubahan ini".
- Kamu ingin lebih sedikit kode.
Pakai Bloc kalau:
- Satu event bisa memicu beberapa transisi state.
- Kamu butuh melacak urutan kejadian untuk debugging atau analitik.
- Kamu butuh mengelola event yang berdatangan cepat — misalnya debounce pada kolom pencarian.
- Beberapa event berbeda menghasilkan state yang mirip, dan kamu ingin membedakannya.
Contoh terakhir itu — mengelola event yang berdatangan cepat — hanya mungkin di Bloc:
dart
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:rxdart/rxdart.dart';
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc(this._repo) : super(const SearchInitial()) {
on<QueryChanged>(
_onQueryChanged,
// Tunggu 300 ms setelah ketikan terakhir, batalkan yang sebelumnya
transformer: (events, mapper) => events
.debounceTime(const Duration(milliseconds: 300))
.switchMap(mapper),
);
}
final SearchRepository _repo;
Future<void> _onQueryChanged(
QueryChanged event,
Emitter<SearchState> emit,
) async {
if (event.kueri.trim().isEmpty) {
emit(const SearchInitial());
return;
}
emit(const SearchLoading());
try {
final hasil = await _repo.cari(event.kueri);
emit(SearchLoaded(hasil));
} catch (_) {
emit(const SearchError('Pencarian gagal.'));
}
}
}Tanpa debounceTime, mengetik "flutter" akan memicu tujuh permintaan jaringan. Dengan itu, hanya satu.
Banyak tim memulai dengan Cubit dan bermigrasi ke Bloc hanya ketika benar membutuhkannya. Keduanya kompatibel dan bisa hidup berdampingan dalam satu proyek.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Keranjang sebagai Cubit dan Bloc — ⭐⭐ · 45–60 menit
Tantangan: Buat satu fitur yang sama dua kali: sekali sebagai Cubit, sekali sebagai Bloc. Fiturnya sederhana — keranjang belanja dengan tambah, kurangi, dan kosongkan — tetapi state-nya harus kustom, bukan sekadar int: ia menyimpan daftar item, total harga, dan status "sedang menghitung ongkir".
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ ┌─────────────────────────────┐ │
│ │ Kopi Susu x2 Rp 50.000 │ │
│ │ Roti Bakar x1 Rp 15.000 │ │
│ └─────────────────────────────┘ │
│ Menghitung ongkir... ⏳ │
│ Total: Rp 65.000 │
│ ▬▬▬▬ │
╰───────────────────────────────────╯emit dengan state yang isinya identik dengan state sekarang → layar di atas tidak rebuild sama sekali, berkat Equatable.
Kriteria selesai:
- State kustom memakai
Equatabledengan seluruh properti masuk keprops. - Setiap perubahan dibuat lewat
copyWithyang menghasilkan objek baru, bukan mengubah objek lama. - Versi
Blocpunya event terpisah untuk setiap aksi, dan handler-nya didaftarkan lewaton<Event>(). - Kamu sudah menguji apa yang terjadi ketika
emitdipanggil dengan state yang isinya identik dengan state sekarang — dan bisa menjelaskan kenapa.
Petunjuk: Kriteria terakhir adalah jantung bab ini. emit mengabaikan state yang sama dengan sebelumnya, dan arti "sama" ditentukan oleh Equatable. Cobalah menghapus satu properti dari props, lalu ubah hanya properti itu: UI tidak akan diperbarui, dan tidak ada satu pun pesan yang memberitahumu kenapa. Sekarang cobalah kebalikannya — hapus Equatable sepenuhnya — dan UI akan dibangun ulang bahkan ketika tidak ada yang berubah. Dua kegagalan ini adalah dua sisi dari konsep yang sama. Soal memilih Cubit atau Bloc setelah latihan ini: perhatikan mana yang lebih ringkas untuk aksi sederhana, dan bayangkan mana yang lebih kamu syukuri ketika suatu saat kamu perlu tahu event apa yang menyebabkan sebuah perubahan.
Variasi 2: Monitor Mesin Pabrik — ⭐⭐ · 40–60 menit
Tantangan: Sebelum menyentuh BLoC sama sekali, tulis dulu Stream murni dengan async* dan yield yang mengeluarkan pembacaan suhu mesin setiap setengah detik, kadang menyisipkan pembacaan yang di luar batas aman. Dengarkan ia dengan await for di satu tempat dan .listen() di tempat lain, lalu bungkus keseluruhannya menjadi sebuah Cubit.
Kriteria selesai:
- Versi
Streammurni berjalan sebagai program Dart biasa lewatdart run, tanpa Flutter. - Kamu bisa menjelaskan perbedaan praktis antara
await fordan.listen()berdasarkan yang kamu alami, bukan dari teori. - Versi
Cubitmengeluarkan state yang membedakan pembacaan normal dari pembacaan berbahaya. - Langganan dibatalkan di
close()yang di-override.
Petunjuk: Bab ini menyebut bahwa BLoC pada dasarnya adalah stream yang dibungkus API yang nyaman, dan latihan ini membuat kalimat itu berhenti menjadi abstraksi. Perbedaan await for dan .listen() yang akan kamu rasakan: yang pertama menahan eksekusi di tempatnya sampai stream selesai, yang kedua mendaftarkan callback lalu langsung lanjut. Itulah alasan .listen() yang dipakai di dalam BLoC — sebuah BLoC tidak boleh berhenti menunggu.
Variasi 3: Unggah Dokumen Bertahap — ⭐⭐⭐ · 45–60 menit
Tantangan: Modelkan proses unggah dokumen dengan sealed class per keadaan: menunggu, memvalidasi, mengunggah dengan persentase, berhasil, dan gagal dengan alasan. Setiap keadaan hanya membawa data yang relevan bagi dirinya — keadaan "menunggu" tidak boleh punya field persentase.
Bayangan tampilan (lima keadaan):
text
Menunggu Memvalidasi Mengunggah 45% Berhasil Gagal
┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌─────────┐ ┌──────────┐
│ [Pilih │ │ Memeriksa │ │▓▓▓▓▓░░░░░ 45%│ │✓ Selesai │ │✗ Ukuran │
│ Berkas] │ │ berkas... │ └──────────────┘ └─────────┘ │terlalu │
└─────────┘ └──────────┘ │besar │
└──────────┘Field persentase hanya ada di keadaan "Mengunggah" — keadaan "Menunggu" tidak mungkin punya field itu, dan compiler yang menjamin itu, bukan konvensi penamaan.
Kriteria selesai:
- Widget memilih tampilan lewat
switchpada state yang diperiksa kelengkapannya oleh compiler, tanpa cabangdefault. - Menambahkan satu keadaan baru membuat compiler menunjukkan setiap tempat yang perlu diperbarui.
- Tidak ada satu pun
nullyang dipakai untuk menandai "field ini tidak berlaku di keadaan ini". - Persentase unggah bergerak halus tanpa membanjiri UI dengan pembaruan berlebihan.
Petunjuk: Gaya sealed class per keadaan dan gaya satu kelas dengan copyWith sama-sama sah, dan bab ini membahas keduanya — yang membedakan adalah apa yang ingin kamu cegah. Satu kelas dengan banyak field nullable membuat kombinasi mustahil bisa terwakili: "sedang memuat sekaligus gagal sekaligus punya data". sealed class membuat kombinasi itu tidak bisa ditulis sama sekali, dan compiler ikut menjaganya.
Variasi 4: Perburuan Jebakan Equatable — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Bangun satu Cubit dengan state yang berisi List, Map, DateTime, dan sebuah objek bersarang yang juga punya Equatable. Lalu buat empat bug perbandingan dengan sengaja, amati gejalanya, dan perbaiki masing-masing: properti terlewat dari props, list yang diubah di tempat, objek bersarang tanpa Equatable, dan DateTime yang dibuat ulang setiap emit.
Kriteria selesai:
- Keempat bug dibuat, gejalanya dicatat, lalu diperbaiki.
- Untuk masing-masing kamu bisa menyebut apakah gejalanya "UI tidak diperbarui" atau "UI dibangun ulang terus-menerus".
- Ada pengujian yang membuktikan dua state dengan isi sama dianggap sama.
BlocObserverdipasang, dan kamu memakainya untuk mengamati emit yang diabaikan.
Petunjuk: Dua gejala di kriteria kedua adalah dua sisi dari kesalahan yang sama, dan mengenali arahnya langsung mempersempit penyebabnya. "UI tidak diperbarui" berarti dua state yang seharusnya berbeda dianggap sama — properti terlewat, atau objek yang diubah di tempat sehingga yang lama dan yang baru adalah objek yang sama persis. "UI dibangun ulang terus-menerus" berarti kebalikannya: dua state yang seharusnya sama dianggap berbeda — biasanya karena ada objek bersarang tanpa Equatable, atau DateTime.now() yang ikut masuk ke props.
Ikhtisar
- Stream adalah aliran data asinkron — banyak nilai dari waktu ke waktu, berbeda dari
Futureyang hanya satu. Dibuat denganasync*+yield, didengarkan denganawait foratau.listen(). - BLoC pada dasarnya adalah stream yang dibungkus dengan API yang nyaman.
Cubitmenyimpan state dan menyediakan metode; ubah state denganemit(...).Blocmenambahkan lapisan event: UI memanggiladd(Event()), dan handler yang didaftarkan lewaton<Event>()memprosesnya.BlocProvidermenyediakan instance,BlocBuildermendengarkan dan membangun ulang,context.read<T>()mengambil tanpa berlangganan.emitmengabaikan state yang sama dengan sebelumnya. Ini fitur, tetapi menjadi sumber bug kalau perbandingan objeknya tidak benar.Equatablemengubah perbandingan dari identitas objek menjadi perbandingan isi. Tanpanya, UI dibangun ulang percuma atau tidak diperbarui sama sekali.- Masukkan semua properti ke
props— properti yang terlewat tidak akan memicu pembaruan. - Selalu buat objek baru (
copyWith,[...list, item]), jangan mengubah yang lama. - Gaya
sealed classper keadaan memungkinkanswitchyang diperiksa kelengkapannya oleh compiler. - Struktur folder BLoC:
data/(models, data_providers, repositories),logic/(cubit, bloc),presentation/(screens, widgets). - Mulai dengan
Cubit; naik keBlocketika butuh jejak event atau pengelolaan aliran event seperti debounce.
Berikutnya: Bab 21 — flutter_bloc: Widget Penghubung, tempat kita membedah BlocProvider, BlocBuilder, BlocListener, dan BlocConsumer.
Transkrip asli
Disintesis dari 2_flutter_bloc-zero-to-hero/1_why-bloc-as-a-state-management.md, 2_bloc-core-concepts.md (stream, cubit, bloc, event), dan 9_comparing-objects-and-equatable-package.md. Lihat PDF BLoC Zero to Hero.