Tampilan
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 │
└───────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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(),
)1
2
3
4
2
3
4
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(),
)1
2
3
4
5
2
3
4
5
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(),
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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(),
),
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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,
);
},
)1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
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}'),
)1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
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.')),
},
)1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
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),
),
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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(),
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Perbedaan penting dari BlocBuilder:
BlocBuilder | BlocListener | |
|---|---|---|
| Membangun ulang UI | Ya | Tidak |
| Dipanggil berapa kali | Setiap rebuild bisa saja | Sekali per perubahan state |
| Untuk | Menggambar | Efek 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();
},
)1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
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(),
)1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
MultiBlocListener
dart
MultiBlocListener(
listeners: [
BlocListener<AuthCubit, AuthState>(
listener: (context, state) { /* ... */ },
),
BlocListener<InternetCubit, InternetState>(
listener: (context, state) { /* ... */ },
),
],
child: const HomeScreen(),
)1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
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,
);
},
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
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'),
),
],
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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() ──┘1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Kalau tidak ditemukan, kamu mendapat error:
Error: Could not find the correct Provider<CounterCubit> above this
HomeScreen Widget.1
2
2
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'),
),
),
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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'),
),
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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'),
);
},
),
)1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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),
),
],
),
],
),
),
),
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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(),
),
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dan BLoC mengambilnya dari sana:
dart
BlocProvider<WeatherCubit>(
create: (context) => WeatherCubit(
context.read<WeatherRepository>(),
),
child: const WeatherScreen(),
)1
2
3
4
5
6
2
3
4
5
6
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 ] │
│ ▬▬▬▬ │
╰───────────────────────────────────╯1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
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. BlocBuildermembungkus sesempit mungkin — hanya tombol dan indikator loading-nya, bukan seluruhScaffold.buildWhendipakai 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
contextyang berada di atasBlocProvider, lalu memperbaikinya denganBuilderatau 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 │
│ ▬▬▬▬ │
╰───────────────────────────────────╯1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
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
debugPrintdi tiapbuilder. - Memilih satu jam tidak membangun ulang seluruh kisi jam.
listenWhenmencegah snackbar "jam sudah dipesan orang lain" muncul berulang untuk state yang sama.- Ringkasan harga memakai
context.select, bukanBlocBuilderpenuh.
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 ulang1
2
3
4
5
6
2
3
4
5
6
Kriteria selesai:
RepositoryProviderberada di atasMultiBlocProvider, yang berada di atasMaterialApp.- Layar yang didorong lewat
Navigatorbisa 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 ditutup1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
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) │
│ ▬▬▬▬ │
╰───────────────────────────────────╯1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
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
listenerdan tidak mengosongkan isian yang sudah diketik. BlocConsumerdipakai 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
BlocProvidermembuat dan menyediakan instance; ia memanggilclose()otomatis saat dilepas. Pakailazy: falsekalau harus langsung aktif.MultiBlocProvidermeratakan penyediaan beberapa BLoC.- Taruh BLoC global di atas
MaterialAppagar layar yang didorong lewatNavigatortetap bisa mengaksesnya. BlocBuilderuntuk menggambar;BlocListeneruntuk efek samping;BlocConsumerkalau butuh keduanya di tempat yang sama.- Jangan tampilkan snackbar atau navigasi di dalam
builder—builderbisa dipanggil berkali-kali untuk state yang sama.listenerdijamin sekali. buildWhendanlistenWhenmembatasi kapan keduanya dijalankan.- Tempatkan
BlocBuildersesempit mungkin, bukan membungkus seluruhScaffold. context.readdi callback,context.watchdibuild,context.selectuntuk satu bagian.context.watchmembangun ulang seluruh widget —BlocBuilderlebih presisi.BuildContextmenunjuk posisi di element tree. Pencarian BLoC berjalan ke atas dari posisi itu.- Jebakan paling umum: memakai
contextyang berada di atasBlocProvider. Perbaiki dengan memecah ke widget terpisah, atau bungkus denganBuilder. - Override
close()pada Cubit untuk membatalkanStreamSubscription— padanandispose(). RepositoryProvideruntuk layanan dan repository; ia tidak memanggilclose().
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.