Bloc adalah library populer untuk state management di Flutter yang memisahkan logika bisnis (business logic) dari antarmuka pengguna (UI). Library ini menyediakan dua metode utama untuk mengelola state: Bloc (yang menggunakan events untuk memicu perubahan state) dan Cubit (versi yang lebih sederhana tanpa events, langsung memanggil fungsi). Kedua metode ini berbasis stream, di mana state diekspos sebagai output yang bisa diamati oleh UI.
Dalam artikel ini, kita akan fokus pada penulisan kelas Bloc/Cubit dan penggunaannya di view (UI Flutter). Sebagai contoh kasus, kita gunakan aplikasi Todo List sederhana, di mana state melibatkan daftar todo item (dengan properti seperti id, deskripsi, dan status selesai).
Catatan: Asumsikan package flutter_bloc
sudah ditambahkan ke pubspec.yaml
dan diimpor di file terkait.
Metode 1: Menggunakan Bloc dengan Events
Metode ini cocok untuk aplikasi kompleks di mana perubahan state perlu dilacak melalui events (seperti aksi pengguna). Kita definisikan: - Events: Kelas-kelas yang merepresentasikan aksi (misalnya, tambah todo, toggle selesai). - States: Kelas yang menyimpan data state (misalnya, list todo, status loading). - Bloc: Kelas yang menghandle events dan emit state baru.
Penulisan Bloc
Definisikan events, states, dan bloc di folder terpisah (misalnya, todos_bloc.dart
).
Events
sealed class TodoEvent {}
final class AddTodo extends TodoEvent {
final String description;
AddTodo(this.description);
}
final class ToggleTodo extends TodoEvent {
final String id;
ToggleTodo(this.id);
}
final class DeleteTodo extends TodoEvent {
final String id;
DeleteTodo(this.id);
}
States
class TodoState {
final List<Todo> todos;
final bool isLoading;
TodoState({this.todos = const [], this.isLoading = false});
TodoState copyWith({List<Todo>? todos, bool? isLoading}) {
return TodoState(
todos: todos ?? this.todos,
isLoading: isLoading ?? this.isLoading,
);
}
}
class Todo {
final String id;
final String description;
final bool isCompleted;
Todo({required this.id, required this.description, this.isCompleted = false});
}
Bloc
class TodoBloc extends Bloc<TodoEvent, TodoState> {
TodoBloc() : super(TodoState()) {
on<AddTodo>(_onAddTodo);
on<ToggleTodo>(_onToggleTodo);
on<DeleteTodo>(_onDeleteTodo);
}
void _onAddTodo(AddTodo event, Emitter<TodoState> emit) {
final newTodo = Todo(id: DateTime.now().toString(), description: event.description);
final updatedTodos = List<Todo>.from(state.todos)..add(newTodo);
emit(state.copyWith(todos: updatedTodos));
}
void _onToggleTodo(ToggleTodo event, Emitter<TodoState> emit) {
final updatedTodos = state.todos.map((todo) {
if (todo.id == event.id) {
return Todo(id: todo.id, description: todo.description, isCompleted: !todo.isCompleted);
}
return todo;
}).toList();
emit(state.copyWith(todos: updatedTodos));
}
void _onDeleteTodo(DeleteTodo event, Emitter<TodoState> emit) {
final updatedTodos = state.todos.where((todo) => todo.id != event.id).toList();
emit(state.copyWith(todos: updatedTodos));
}
}
Penggunaan di View
Gunakan widget dari flutter_bloc
seperti BlocProvider
(untuk provide bloc ke subtree), BlocBuilder
(untuk rebuild UI berdasarkan state), dan context.read<Bloc>()
untuk add event.
Provide Bloc di Level App
Di main.dart
:
void main() {
runApp(
BlocProvider(
create: (context) => TodoBloc(),
child: MyApp(),
),
);
}
Gunakan di View
Di TodoListScreen.dart
:
class TodoListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
if (state.isLoading) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) {
final todo = state.todos[index];
return ListTile(
title: Text(todo.description),
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => context.read<TodoBloc>().add(ToggleTodo(todo.id)),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => context.read<TodoBloc>().add(DeleteTodo(todo.id)),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Misalnya, tampilkan dialog untuk input description, lalu add event
context.read<TodoBloc>().add(AddTodo('New Todo'));
},
child: Icon(Icons.add),
),
);
}
}
Metode 2: Menggunakan Cubit (Tanpa Events)
Metode ini lebih sederhana untuk kasus di mana tidak perlu tracking events secara detail. Kita hanya definisikan state dan fungsi langsung di Cubit untuk emit state baru.
Penulisan Cubit
Definisikan state dan cubit di file terpisah (misalnya, todos_cubit.dart
).
States
// Sama seperti di atas: TodoState dan Todo class
Cubit
class TodoCubit extends Cubit<TodoState> {
TodoCubit() : super(TodoState());
void addTodo(String description) {
final newTodo = Todo(id: DateTime.now().toString(), description: description);
final updatedTodos = List<Todo>.from(state.todos)..add(newTodo);
emit(state.copyWith(todos: updatedTodos));
}
void toggleTodo(String id) {
final updatedTodos = state.todos.map((todo) {
if (todo.id == id) {
return Todo(id: todo.id, description: todo.description, isCompleted: !todo.isCompleted);
}
return todo;
}).toList();
emit(state.copyWith(todos: updatedTodos));
}
void deleteTodo(String id) {
final updatedTodos = state.todos.where((todo) => todo.id != id).toList();
emit(state.copyWith(todos: updatedTodos));
}
}
Penggunaan di View
Sama seperti Bloc, tapi panggil fungsi langsung alih-alih add event.
Provide Cubit di Level App
Di main.dart
:
void main() {
runApp(
BlocProvider(
create: (context) => TodoCubit(),
child: MyApp(),
),
);
}
Gunakan di View
Di TodoListScreen.dart
:
class TodoListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: BlocBuilder<TodoCubit, TodoState>(
builder: (context, state) {
if (state.isLoading) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) {
final todo = state.todos[index];
return ListTile(
title: Text(todo.description),
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => context.read<TodoCubit>().toggleTodo(todo.id),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => context.read<TodoCubit>().deleteTodo(todo.id),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Misalnya, tampilkan dialog untuk input, lalu panggil fungsi
context.read<TodoCubit>().addTodo('New Todo');
},
child: Icon(Icons.add),
),
);
}
}
Kesimpulan
Bloc dengan events lebih cocok untuk traceability dan kompleksitas tinggi, sementara Cubit ideal untuk simplicity. Kedua metode menggunakan widget seperti BlocProvider
dan BlocBuilder
untuk integrasi dengan UI. Untuk fitur lanjutan seperti persistensi, gunakan HydratedBloc
, tapi itu di luar fokus artikel ini.