BLoC Pattern trong Flutter
1. Giới thiệu
BLoC (Business Logic Component) tách biệt business logic khỏi UI:
UI ──Events──▶ BLoC ──States──▶ UI# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.02. Các khái niệm cốt lõi
| Khái niệm | Mô tả |
|---|---|
| Event | Input từ UI (user actions) |
| State | Output cho UI (data to display) |
| BLoC | Xử lý Events và emit States |
3. Ví dụ cơ bản - Counter
Định nghĩa Events
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}Định nghĩa States
class CounterState {
final int count;
const CounterState(this.count);
}Định nghĩa BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<IncrementEvent>((event, emit) {
emit(CounterState(state.count + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterState(state.count - 1));
});
}
}4. Provide BLoC
void main() {
runApp(
BlocProvider(
create: (context) => CounterBloc(),
child: MyApp(),
),
);
}
// Hoặc multiple
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => CounterBloc()),
BlocProvider(create: (_) => AuthBloc()),
],
child: MyApp(),
)5. Consume BLoC
BlocBuilder
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('Count: ${state.count}');
},
)context.watch / context.read
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// watch: rebuild khi state thay đổi
Text('${context.watch<CounterBloc>().state.count}'),
ElevatedButton(
// read: gửi event
onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
child: Text('+'),
),
],
);
}
}6. BlocListener - Side Effects
Không rebuild UI, chỉ thực hiện side effects:
BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthSuccess) {
Navigator.pushReplacementNamed(context, '/home');
} else if (state is AuthFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error)),
);
}
},
child: LoginForm(),
)7. BlocConsumer - Builder + Listener
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
// Side effects
if (state is AuthFailure) {
ScaffoldMessenger.of(context).showSnackBar(...);
}
},
builder: (context, state) {
// UI
if (state is AuthLoading) return CircularProgressIndicator();
return LoginForm();
},
)8. Ví dụ thực tế - Todo BLoC
Events
abstract class TodoEvent {}
class LoadTodos extends TodoEvent {}
class AddTodo extends TodoEvent {
final String title;
AddTodo(this.title);
}
class ToggleTodo extends TodoEvent {
final String id;
ToggleTodo(this.id);
}
class DeleteTodo extends TodoEvent {
final String id;
DeleteTodo(this.id);
}States
abstract class TodoState {}
class TodoInitial extends TodoState {}
class TodoLoading extends TodoState {}
class TodoLoaded extends TodoState {
final List<Todo> todos;
TodoLoaded(this.todos);
}
class TodoError extends TodoState {
final String message;
TodoError(this.message);
}BLoC
class TodoBloc extends Bloc<TodoEvent, TodoState> {
final TodoRepository repository;
TodoBloc(this.repository) : super(TodoInitial()) {
on<LoadTodos>(_onLoadTodos);
on<AddTodo>(_onAddTodo);
on<ToggleTodo>(_onToggleTodo);
on<DeleteTodo>(_onDeleteTodo);
}
Future<void> _onLoadTodos(LoadTodos event, Emitter<TodoState> emit) async {
emit(TodoLoading());
try {
final todos = await repository.getTodos();
emit(TodoLoaded(todos));
} catch (e) {
emit(TodoError(e.toString()));
}
}
void _onAddTodo(AddTodo event, Emitter<TodoState> emit) {
if (state is TodoLoaded) {
final currentTodos = (state as TodoLoaded).todos;
final newTodo = Todo(id: DateTime.now().toString(), title: event.title);
emit(TodoLoaded([...currentTodos, newTodo]));
}
}
// ... other handlers
}UI
class TodoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
if (state is TodoLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is TodoError) {
return Center(child: Text(state.message));
}
if (state is TodoLoaded) {
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) {
final todo = state.todos[index];
return ListTile(
title: Text(todo.title),
onTap: () => context.read<TodoBloc>().add(ToggleTodo(todo.id)),
);
},
);
}
return Container();
},
),
);
}
}9. Cubit - Simplified BLoC
Không cần Events, gọi methods trực tiếp:
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
// Sử dụng
context.read<CounterCubit>().increment();10. Khi nào dùng BLoC?
| Use case | Recommendation |
|---|---|
| Simple state | setState hoặc Provider |
| Medium complexity | Cubit hoặc Riverpod |
| Complex, event-driven | BLoC |
| Large team, enterprise | BLoC |
📝 Tóm tắt
| Component | Vai trò |
|---|---|
Event | User actions |
State | UI data |
Bloc | Event → State transformation |
Cubit | Simplified Bloc |
BlocBuilder | Rebuild UI on state change |
BlocListener | Side effects |
BlocConsumer | Builder + Listener |
Last updated on