Tampilan
Bab 23 — Akses BLoC & Navigasi
Menyediakan sebuah BLoC untuk satu layar itu mudah. Yang sering menjebak adalah mempertahankan akses ke instance yang sama ketika pengguna berpindah antar layar — karena Navigator bekerja dengan cara yang tidak selalu intuitif.
Bab ini membahas tiga cakupan akses, dan mengapa satu kesalahan kecil bisa membuat penghitungmu tiba-tiba kembali ke nol.
Kenapa navigasi menyulitkan
Ingat dari Bab 21 bahwa context.read<T>() menelusuri element tree ke atas mencari BlocProvider<T> terdekat.
Masalahnya: ketika kamu memanggil Navigator.push, layar baru tidak ditempatkan sebagai anak dari layar sebelumnya. Ia didorong sebagai rute baru yang bercabang langsung dari Navigator — yang berada di dalam MaterialApp.
MaterialApp
│
Navigator ◄─────────── semua rute bercabang dari sini
│
├── Route 1: HomeScreen
│ │
│ BlocProvider<CounterCubit> ← disediakan DI SINI
│ │
│ Isi HomeScreen
│
└── Route 2: SecondScreen ← didorong dengan push()
│
Isi SecondScreen
│
context.read<CounterCubit>() 💥 TIDAK DITEMUKANSecondScreen bukan keturunan dari BlocProvider itu. Pencarian ke atas melewati Navigator, lalu MaterialApp, dan tidak menemukan apa pun.
Errornya:
Error: Could not find the correct Provider<CounterCubit> above this
SecondScreen Widget.Ada tiga cakupan akses yang menyelesaikan ini, masing-masing untuk situasi yang berbeda.
1. Akses lokal
Cakupan paling sempit: BLoC hanya hidup selama satu layar terbuka, dan dibuang ketika layar ditutup.
dart
class TodoScreen extends StatelessWidget {
const TodoScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<TodoCubit>(
create: (_) => TodoCubit(),
child: const _IsiTodoScreen(),
);
}
}
class _IsiTodoScreen extends StatelessWidget {
const _IsiTodoScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Tugas')),
body: BlocBuilder<TodoCubit, TodoState>(
builder: (context, state) => DaftarTugas(state: state),
),
);
}
}Ini pilihan yang tepat untuk state yang memang hanya relevan di satu layar: formulir, hasil pencarian, keranjang sementara.
Keuntungannya: memori dibersihkan otomatis ketika layar ditutup, dan setiap kali layar dibuka kamu mendapat state yang bersih.
Perhatikan pemisahan menjadi _IsiTodoScreen. Ini bukan sekadar kerapian — ini solusi untuk masalah context yang dibahas di Bab 21: widget di dalam BlocProvider butuh context-nya sendiri.
2. Akses rute
Kamu punya BLoC di layar A, dan ingin layar B memakai instance yang sama. Untuk ini ada BlocProvider.value.
dart
// Di HomeScreen
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctxBaru) => BlocProvider.value(
// Ambil instance yang SUDAH ADA, jangan buat baru
value: context.read<CounterCubit>(),
child: const SecondScreen(),
),
),
);
},
child: const Text('Buka Layar Kedua'),
)Perhatikan dengan teliti: context.read<CounterCubit>() dipanggil memakai context milik HomeScreen — yang berada di bawah BlocProvider. Sementara ctxBaru di dalam builder adalah context rute baru yang tidak punya akses.
Navigator
├── Route 1: HomeScreen
│ │
│ BlocProvider<CounterCubit> ─── instance A
│ │
│ Tombol ──── context.read() ✅
│
└── Route 2
│
BlocProvider.value(value: instance A) ← instance yang SAMA
│
SecondScreen ──── context.read() ✅Jangan pakai create untuk instance yang sudah ada
dart
// ❌ Membuat instance BARU — state layar A dan B terpisah
MaterialPageRoute(
builder: (_) => BlocProvider<CounterCubit>(
create: (_) => CounterCubit(), // 💥 instance baru, state kembali 0
child: const SecondScreen(),
),
)
// ✅ Memakai instance yang sudah ada
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: context.read<CounterCubit>(),
child: const SecondScreen(),
),
)Ini bug yang sangat membingungkan ketika terjadi: penghitungmu menunjukkan 5 di layar pertama, tetapi 0 di layar kedua — dan tidak ada error apa pun.
Perbedaan penting lainnya: BlocProvider.value tidak memanggil close(). Ia tahu bahwa instance itu dimiliki orang lain, jadi ia tidak berhak membuangnya. BlocProvider dengan create yang memilikinya, dan ia yang akan menutupnya.
3. Akses global
Untuk BLoC yang dibutuhkan di mana-mana — autentikasi, tema, status koneksi — sediakan di atas MaterialApp.
dart
// lib/main.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()),
],
// MaterialApp berada DI BAWAH provider,
// sehingga Navigator dan semua rutenya ikut tercakup
child: MaterialApp(
title: 'Aplikasi',
debugShowCheckedModeBanner: false,
home: const HomeScreen(),
),
);
}
}Sekarang pohonnya menjadi:
MultiBlocProvider ◄────────── di sini semua BLoC hidup
│
MaterialApp
│
Navigator
├── Route 1: HomeScreen ──► context.read() ✅
├── Route 2: SecondScreen ──► context.read() ✅
└── Route 3: ThirdScreen ──► context.read() ✅Karena Navigator berada di bawah provider, setiap rute yang didorongnya otomatis punya akses.
Kapan global, kapan tidak
Menyediakan segalanya secara global itu menggoda karena "pasti berfungsi". Tetapi itu berarti:
- Setiap BLoC hidup selama aplikasi berjalan, memakan memori.
- State tidak pernah bersih — pengguna keluar akun, tetapi data lama masih ada.
- Sulit melacak siapa memakai apa.
Aturan praktis: global hanya untuk yang benar-benar lintas aplikasi — auth, tema, koneksi, keranjang belanja. Sisanya lokal atau per rute.
Named routes dengan BLoC
Untuk aplikasi dengan banyak layar, memusatkan definisi rute membuat pengelolaan BLoC jauh lebih rapi.
dart
// lib/presentation/router/app_router.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../screens/home_screen.dart';
import '../screens/second_screen.dart';
import '../screens/third_screen.dart';
import '../../logic/cubit/counter_cubit.dart';
class AppRouter {
Route? bangunRute(RouteSettings settings) {
return switch (settings.name) {
'/' => MaterialPageRoute(
builder: (_) => const HomeScreen(
title: 'Beranda',
warna: Colors.blueAccent,
),
),
'/second' => MaterialPageRoute(
builder: (_) => const SecondScreen(
title: 'Layar Kedua',
warna: Colors.redAccent,
),
),
'/third' => MaterialPageRoute(
builder: (_) => const ThirdScreen(
title: 'Layar Ketiga',
warna: Colors.greenAccent,
),
),
_ => null,
};
}
}dart
// lib/main.dart
class AplikasiSaya extends StatefulWidget {
const AplikasiSaya({super.key});
@override
State<AplikasiSaya> createState() => _AplikasiSayaState();
}
class _AplikasiSayaState extends State<AplikasiSaya> {
// Router dibuat SEKALI, bukan di dalam build()
final _router = AppRouter();
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CounterCubit>(create: (_) => CounterCubit()),
BlocProvider<InternetCubit>(
create: (_) => InternetCubit(connectivity: Connectivity()),
),
],
child: MaterialApp(
title: 'Aplikasi BLoC',
debugShowCheckedModeBanner: false,
onGenerateRoute: _router.bangunRute,
initialRoute: '/',
),
);
}
}Karena BLoC disediakan secara global, setiap layar bisa memakai Navigator.pushNamed tanpa memikirkan BlocProvider.value sama sekali:
dart
ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed('/second'),
child: const Text('Ke Layar Kedua'),
)Router harus dibuat sekali
Perhatikan bahwa _router adalah properti final di kelas State, bukan dibuat di dalam build(). Membuatnya ulang setiap build berarti objek baru setiap kali — sama seperti masalah GlobalKey di Bab 6.
Navigasi dari dalam BLoC
Pertanyaan yang sering muncul: bolehkah BLoC melakukan navigasi sendiri?
Jawabannya tidak. Navigasi adalah urusan presentation. BLoC yang memanggil Navigator akan butuh BuildContext, yang berarti ia harus mengimpor Flutter — melanggar aturan di Bab 22.
Cara yang benar: BLoC mengeluarkan state, dan BlocListener yang bereaksi.
dart
// ❌ SALAH — BLoC tidak boleh tahu tentang navigasi
class AuthCubit extends Cubit<AuthState> {
Future<void> masuk(BuildContext context, String email, String sandi) async {
// ...
Navigator.of(context).pushReplacement(...); // 💥
}
}dart
// ✅ BENAR — BLoC hanya mengeluarkan state
class AuthCubit extends Cubit<AuthState> {
AuthCubit(this._repository) : super(const AuthInitial());
final AuthRepository _repository;
Future<void> masuk(String email, String sandi) async {
emit(const AuthLoading());
try {
final pengguna = await _repository.masuk(email, sandi);
emit(AuthSuccess(pengguna));
} on KesalahanAuth catch (e) {
emit(AuthFailure(e.pesan));
}
}
}dart
// ✅ UI yang memutuskan apa artinya state itu
BlocListener<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthSuccess) {
Navigator.of(context).pushNamedAndRemoveUntil('/home', (_) => false);
} else if (state is AuthFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.pesan)),
);
}
},
child: const FormMasuk(),
)Keuntungannya nyata: AuthCubit bisa diuji tanpa membangun satu widget pun, dan bisa dipakai ulang di aplikasi lain dengan alur navigasi yang berbeda.
Navigasi reaktif berbasis state
Untuk alur autentikasi, ada pola yang lebih bersih daripada BlocListener: biarkan struktur widget itu sendiri mencerminkan state.
dart
// lib/presentation/screens/auth_gate.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
return switch (state) {
AuthInitial() || AuthLoading() => const SplashScreen(),
AuthSuccess() => const HomeScreen(),
AuthFailure() || AuthLoggedOut() => const LoginScreen(),
};
},
);
}
}Tidak ada Navigator.push sama sekali. Ketika pengguna keluar akun, AuthCubit mengeluarkan AuthLoggedOut, dan LoginScreen otomatis muncul — seluruh tumpukan navigasi ikut hilang karena widget-nya diganti.
Pola ini menghilangkan seluruh kelas bug seperti "pengguna sudah keluar tetapi masih bisa menekan tombol kembali ke beranda".
Mengirim data lewat rute
Ketika layar butuh argumen:
dart
// Mengirim
Navigator.of(context).pushNamed('/detail', arguments: idProduk);dart
// Di router
Route? bangunRute(RouteSettings settings) {
return switch (settings.name) {
'/detail' => MaterialPageRoute(
builder: (_) {
final id = settings.arguments as String;
return BlocProvider<DetailCubit>(
create: (ctx) => DetailCubit(
ctx.read<ProdukRepository>(),
)..muat(id),
child: const DetailScreen(),
);
},
),
_ => null,
};
}Perhatikan ..muat(id) — operator cascade dari Bab 2 yang membuat cubit langsung memuat datanya begitu dibuat.
Untuk keamanan tipe yang lebih baik, lebih disukai mengoper lewat konstruktor:
dart
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => BlocProvider<DetailCubit>(
create: (c) => DetailCubit(c.read<ProdukRepository>())..muat(idProduk),
child: DetailScreen(idProduk: idProduk),
),
),
);Membersihkan state saat keluar akun
Masalah nyata yang sering terlewat: pengguna A keluar, pengguna B masuk, dan data pengguna A masih terlihat karena BLoC global tidak pernah direset.
dart
class AuthCubit extends Cubit<AuthState> {
AuthCubit(this._repository) : super(const AuthInitial());
final AuthRepository _repository;
Future<void> keluar() async {
await _repository.keluar();
emit(const AuthLoggedOut());
}
}dart
// Di level aplikasi, dengarkan dan bersihkan BLoC lain
BlocListener<AuthCubit, AuthState>(
listenWhen: (sebelum, sesudah) => sesudah is AuthLoggedOut,
listener: (context, state) {
context.read<KeranjangCubit>().kosongkan();
context.read<NotifikasiCubit>().reset();
context.read<ProfilCubit>().reset();
},
child: const AuthGate(),
)Alternatif yang lebih bersih untuk aplikasi besar: jangan sediakan BLoC berbasis pengguna secara global. Sediakan di bawah AuthGate, sehingga ketika pengguna keluar dan widget-nya diganti, BLoC-nya ikut dibuang otomatis.
dart
AuthSuccess() => MultiBlocProvider(
providers: [
// BLoC ini lahir saat login, mati saat logout
BlocProvider(create: (c) => KeranjangCubit(c.read<KeranjangRepo>())),
BlocProvider(create: (c) => ProfilCubit(c.read<ProfilRepo>())),
],
child: const HomeScreen(),
),Ini pemakaian arsitektur untuk menyelesaikan masalah yang biasanya diselesaikan dengan kode pembersihan manual.
Ringkasan tiga cakupan
┌─────────────────────────────────────────────────────────────────┐
│ LOKAL — BlocProvider(create:) di dalam satu layar │
│ Hidup: selama layar terbuka │
│ Untuk: formulir, pencarian, state sementara │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ RUTE — BlocProvider.value(value:) saat push │
│ Hidup: dimiliki layar asal │
│ Untuk: berbagi instance antara dua layar tertentu │
│ ⚠️ Tidak memanggil close() │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ GLOBAL — MultiBlocProvider di atas MaterialApp │
│ Hidup: selama aplikasi berjalan │
│ Untuk: auth, tema, koneksi, keranjang │
└─────────────────────────────────────────────────────────────────┘Diagram keputusannya:
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Keranjang ke Checkout — ⭐⭐ · 30–45 menit
Tantangan: Bangun alur dua layar: daftar keranjang → layar checkout. Layar checkout harus melihat instance keranjang yang sama dengan layar daftar, sehingga mengubah jumlah item di checkout langsung terlihat saat kembali. Lalu tambahkan tombol keluar akun yang membersihkan seluruh state dan membuat tombol kembali tidak bisa membawa pengguna ke halaman mana pun sebelumnya.
Bayangan alur layar:
text
[Daftar Keranjang: 2 item] ──push(.value)──▶ [Checkout: 2 item]
▲ (INSTANCE SAMA) │
└──── ubah jumlah di checkout ────────────────┘
kembali ke daftar → angka baru sudah terlihat di sini...versi salah (create alih-alih .value) yang sengaja kamu coba lebih dulu:
text
[Daftar Keranjang: 2 item] ──push(create)──▶ [Checkout: 0 item]
← keranjang KOSONG,
tanpa error apa punKriteria selesai:
- Layar checkout menerima BLoC lewat
BlocProvider.value, dengancontext.readyang dipanggil memakai context layar asal. - Kamu sudah mencoba versi salahnya — memakai
createalih-alih.value— dan mengamati bahwa layar kedua muncul dengan keranjang kosong tanpa satu pun pesan error. - BLoC autentikasi berada di atas
MaterialApp, sementara BLoC keranjang tidak. - Keluar akun ditangani lewat navigasi reaktif — sebuah gerbang yang mengawasi state autentikasi — bukan dengan
pushAndRemoveUntilmanual di dalam callback tombol.
Petunjuk: Kunci untuk memahami bab ini adalah menyadari bahwa Navigator.push tidak menempatkan layar baru sebagai anak dari layar sekarang. Ia menempatkannya sebagai cabang baru dari Navigator, yang letaknya jauh lebih tinggi di pohon — karena itu ia tidak bisa "melihat ke atas" dan menemukan BlocProvider yang kamu pasang di layar asal. BlocProvider.value menyelesaikannya dengan memasang ulang instance yang sama di cabang baru itu. Perhatikan bahwa ia sengaja tidak memanggil close(), karena instance-nya milik orang lain — memanggil close() di sana akan mematikan keranjangmu saat checkout ditutup.
Variasi 2: Tiga Cakupan di Satu Aplikasi — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun kerangka aplikasi bengkel yang memakai ketiga cakupan sekaligus dan bisa kamu benarkan satu per satu: sesi pengguna sebagai BLoC global, daftar antrean servis sebagai BLoC per rute, dan formulir estimasi biaya sebagai BLoC lokal satu layar.
Bayangan cakupan ketiga BLoC:
text
Global (di atas MaterialApp) → Sesi Pengguna
hidup selama aplikasi berjalan
Per rute (BlocProvider.value) → Antrean Servis
dibagi ke layar-layar terkait, dibuang saat rute ditutup
Lokal (BlocProvider create) → Estimasi Biaya
hidup untuk satu layar formulir saja, dibuang saat layar ditutupKriteria selesai:
- Ketiganya ada, dan untuk masing-masing kamu bisa menjelaskan dalam satu kalimat kenapa cakupannya bukan yang lain.
- BLoC lokal benar-benar dibuang saat layarnya ditutup — dibuktikan dengan
BlocObserver. - BLoC global tetap hidup saat berpindah-pindah layar.
- Tidak ada satu pun BLoC yang dijadikan global hanya karena itu lebih mudah.
Petunjuk: Kriteria terakhir adalah godaan terbesar bab ini: menaruh semuanya di atas MaterialApp membuat semua masalah akses hilang seketika. Harganya baru terasa nanti — state yang tidak pernah dibersihkan, kebocoran antar pengguna setelah keluar akun, dan BLoC yang tetap bekerja padahal layarnya sudah lama ditutup. Aturan praktisnya: global hanya untuk yang benar-benar lintas aplikasi, seperti sesi dan tema.
Variasi 3: Gerbang Reaktif dan Pembersihan — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun aplikasi dengan tiga keadaan sesi — belum diperiksa, masuk, keluar — dan sebuah gerbang yang memilih layar berdasarkan itu. Lalu pastikan keluar akun benar-benar membersihkan seluruh state berbasis pengguna, dan masuk dengan akun lain tidak memperlihatkan satu pun data pengguna sebelumnya.
Bayangan alur layar:
text
Sesi: belum diperiksa ──▶ [Splash]
Sesi: masuk ──▶ [Beranda Pengguna A]
Sesi: keluar ──▶ [Layar Masuk]masuk sebagai A → keluar → masuk sebagai B:
text
[Beranda Pengguna A] ──keluar──▶ [Masuk] ──masuk sbg B──▶ [Beranda Pengguna B]
(TIDAK ada sisa
data milik A)Kriteria selesai:
- Gerbang memakai
switchpada state sesi tanpa cabangdefault. - Keadaan "belum diperiksa" menampilkan splash, sehingga layar masuk tidak berkedip saat sesi sedang dipulihkan.
- Masuk sebagai pengguna A, keluar, lalu masuk sebagai pengguna B tidak menampilkan satu pun data milik A.
- Tidak ada
pushAndRemoveUntildi seluruh alur autentikasi.
Petunjuk: Kriteria ketiga adalah bug yang sangat sering lolos ke produksi, karena hanya muncul kalau kamu benar-benar mengujinya dengan dua akun. Ada dua pendekatan: membersihkan tiap BLoC berbasis pengguna saat keluar, atau — biasanya lebih andal — membuat ulang seluruh subpohon yang berisi BLoC itu, sehingga tidak ada yang perlu diingat untuk dibersihkan. Yang kedua bekerja dengan sendirinya kalau gerbangmu memang menukar subpohon berdasarkan state.
Variasi 4: Rute Bernama dengan BLoC — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Bangun aplikasi lima layar dengan rute bernama, di mana dua di antaranya menerima argumen dan tiga di antaranya butuh BLoC dari layar sebelumnya. Tangani juga kasus rute yang dibuka langsung dari deep link tanpa melewati layar induknya.
Bayangan alur layar:
text
Alur normal: [Daftar] ──push('/detail')──▶ [Detail]
(BLoC ada) (pakai BLoC yang sama)
Dari deep link (aplikasi baru dibuka langsung ke /detail):
[Detail]
│ BLoC tidak ditemukan
▼ → buat BARU sendiri
(tetap berfungsi normal)Argumen salah tipe atau hilang → [Layar Kesalahan], bukan crash merah.
Kriteria selesai:
- Argumen rute divalidasi, dan argumen yang salah tipe atau hilang menghasilkan layar kesalahan, bukan crash.
- Layar yang dibuka dari deep link tanpa induknya tetap berfungsi — ia membuat BLoC-nya sendiri kalau tidak menemukan yang sudah ada.
- Tabel rute dibuat sekali sebagai properti
final, bukan di dalambuild(). - Kamu bisa menyebut satu hal yang hilang dibandingkan
MaterialPageRoute.
Petunjuk: Kriteria kedua adalah persoalan nyata yang muncul begitu aplikasimu punya deep link atau notifikasi yang membuka layar tertentu: layar detail yang biasanya menerima BLoC dari daftar tiba-tiba dibuka sendirian. Pola yang bekerja adalah membuat layar itu mampu berdiri sendiri — coba ambil BLoC yang ada, dan buat baru kalau tidak ketemu. Ini juga membuat layar itu jauh lebih mudah diuji.
Ikhtisar
Navigator.pushmenempatkan layar baru sebagai rute yang bercabang dariNavigator, bukan sebagai anak layar sebelumnya — karena itu ia kehilangan akses keBlocProviderdi layar asal.- Akses lokal (
BlocProvider(create:)) — BLoC hidup selama satu layar, dibuang otomatis saat ditutup. - Akses rute (
BlocProvider.value(value:)) — berbagi instance yang sama ke layar baru. Panggilcontext.readmemakai context layar asal. BlocProvider.valuetidak memanggilclose()karena instance-nya dimiliki orang lain.- Memakai
createalih-alih.valueuntuk layar kedua menghasilkan instance baru dengan state kosong — bug yang tidak menimbulkan error apa pun. - Akses global — taruh
MultiBlocProviderdi atasMaterialAppagarNavigatordan seluruh rutenya tercakup. - Global hanya untuk yang benar-benar lintas aplikasi. Sisanya lokal atau per rute.
- Buat objek router sekali sebagai properti
final, bukan di dalambuild(). - BLoC tidak boleh melakukan navigasi. Ia mengeluarkan state;
BlocListeneratauBlocBuilderyang memutuskan artinya. - Navigasi reaktif (
AuthGatedenganswitchpada state) menghilangkan seluruh kelas bug seperti bisa kembali ke halaman setelah keluar akun. - Bersihkan BLoC berbasis pengguna saat keluar akun — atau lebih baik, sediakan di bawah gerbang autentikasi agar terbuang otomatis.
Berikutnya: Bab 24 — Komunikasi Antar-BLoC.
Transkrip asli
Disintesis dari 2_flutter_bloc-zero-to-hero/6_bloc-access-and-navigation.md (local, route, dan global access dengan BlocProvider serta BlocProvider.value) dan 8_build-context-in-depth.md. Lihat PDF BLoC Zero to Hero.