State Management dengan Kombinasi Bloc dan Freezed
Apa itu Bloc dan Cubit
Bloc pada flutter adalah state management yang menerapkan Design Pattern Business Logic Component. Bloc memisahkan presentation layer dari Business Logic. Pada Bloc UI menerima dan mengirim data menggunakan Stream. Artikel ini akan menggunakan Cubit yang merupakan versi lebih sederhana dari Bloc. Perbedaan Cubit dan Bloc adalah Cubit menggunakan function dari UI untuk meminta data ke Business Logic.
Apa itu Freezed
Freezed adalah code generator di dart untuk membuat data class dan union. Artikel ini menggunakan freezed sebagai state dari Cubit. Dengan memanfaatkan fitur pattern matching dari freezed, developer dapat membuat dan memetakan state ke ui dengan mudah.
Aplikasi
Pada tutorial ini kita akan membuat contoh penggunaan Cubit dan Freezed dengan memanggil sebuah fungsi async untuk mensimulasi pemanggilan data dari internet.
Install Dependency
Install package-package berikut sebagai dependency
- flutter_bloc
- freezed_annotation
Install package package berikut sebagai dev
- build_runner
- freezed
Struktur Project
lib/
├─ cubit/
│ ├─ fake_http_call_cubit.dart
│ ├─ fake_http_call_cubit.freezed.dart
│ ├─ fake_http_call_state.dart
├─ home_page.dart
├─ main.dart
Membuat Cubit
Pertama buat file cubit dengan statenya fake_http_call_cubit.dart
part 'fake_http_call_state.dart';
part 'fake_http_call_cubit.freezed.dart';
class FakeHttpCallCubit extends Cubit<FakeHttpCallState> {
FakeHttpCallCubit() : super(const FakeHttpCallState.init());
void fetchData() async {
//Todo
}
void fetchDataEmpty() async {
//TODO
}
void fetchDataError() async {
//TODO
}
}
Membuat class state dengan freezed
fake_http_call_state.dart
part of 'fake_http_call_cubit.dart';
@freezed
class FakeHttpCallState with _$FakeHttpCallState {
const factory FakeHttpCallState.init() = _Init;
const factory FakeHttpCallState.loading() = _Loading;
const factory FakeHttpCallState.noData() = _NoData;
const factory FakeHttpCallState.hasData(List<String> languages) = _HasData;
const factory FakeHttpCallState.error(String message) = _Error;
}
Setiap factory di dalam class tersebut merepresentasikan setiap state untuk FakeHttpCallCubit. State loading mampu menampung data berupa List String. Sedangkan state error mampu menampung data String.
Build Runner
Jalankan build runner dengan menjalankan command ini di terminal
flutter pub run build_runner build
Apabila build runner berhasil maka file fake_http_call_cubit.freezed.dart akan terbentuk dan peringatan error dari IDE akan hilang
Lengkapi Cubit
Isikan method-method pada cubit menjadi seperti ini
void fetchData() async {
emit(FakeHttpCallState.loading());
await Future.delayed(const Duration(seconds: 1));
emit(FakeHttpCallState.hasData(["Dart", "Kotlin", "dart", "Rust"]));
}
void fetchDataEmpty() async {
emit(FakeHttpCallState.loading());
await Future.delayed(const Duration(seconds: 1));
emit(FakeHttpCallState.noData());
}
void fetchDataError() async {
emit(FakeHttpCallState.loading());
await Future.delayed(const Duration(seconds: 1));
emit(FakeHttpCallState.error("Gagal terhubung ke server"));
}
Method emit dari Cubit digunakan untuk memperbarui state. contoh pada method fetchData, emit dipanggil untuk mengganti state menjadi loading.
Kemudian untuk mensimulasikan waktu loading digunakanlah Future.delayed sepanjang 1 detik. Setelah itu di emit lah state hasData dengan isi List String.
UI Layer
Suntikan cubit ke widget HomePage dengan BlocProvider
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider(
create: (context) => FakeHttpCallCubit(),
child: const HomePage(),
),
);
}
}
Buat halaman HomePage
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//TODO
_content(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
context.read<FakeHttpCallCubit>().fetchData();
},
child: const Text('Fetch Data'),
),
ElevatedButton(
onPressed: () {
context.read<FakeHttpCallCubit>().fetchDataEmpty();
},
child: const Text('Fetch Data Kosong'),
),
ElevatedButton(
onPressed: () {
context.read<FakeHttpCallCubit>().fetchDataError();
},
child: const Text('Fetch Error'),
),
],
)
],
),
);
}
Consume FakeHttpCallCubit dengan menggunakan BlocBuilder di dalam helper method _content()
Widget _content() {
return BlocBuilder<FakeHttpCallCubit, FakeHttpCallState>(
builder: (context, state) {
return state.when(
init: () => Container(),
loading: () => const CircularProgressIndicator(),
noData: () => const Text(
'Data Kosong',
style: TextStyle(fontSize: 24),
),
hasData: (languages) {
return Column(
mainAxisSize: MainAxisSize.min,
children: List.generate(
languages.length,
(index) => Text(
languages[index],
style: const TextStyle(fontSize: 24),
),
),
);
},
error: (message) => Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error),
Text(
message,
style: const TextStyle(fontSize: 24),
),
],
),
);
},
);
}
Method .when merupakan pattern matching yang memetakan setiap state agar mereturn nilai yang dalam hal ini adalah widget. State init mereturn Container(), Loading mereturn CircularProgressIndicator. hasData mereturn Column berisi List Text. dan Error mereturn Icon dan Text yang berisi pesan error.