Quản Lý Trạng Thái (State Management) trong Flutter: Hướng Dẫn Chi Tiết
1. Tại Sao Cần Quản Lý Trạng Thái?
Trong Flutter, trạng thái (state) là dữ liệu có thể thay đổi trong quá trình chạy ứng dụng. Ví dụ:
- Dữ liệu người dùng nhập vào form.
- Danh sách các mục được hiển thị trên màn hình.
- Trạng thái đăng nhập của người dùng.
Khi trạng thái thay đổi, giao diện người dùng (UI) cần được cập nhật để phản ánh những thay đổi đó. Quản lý trạng thái giúp bạn kiểm soát cách dữ liệu được truyền và cập nhật trong ứng dụng, đảm bảo UI luôn đồng bộ với trạng thái hiện tại.
2. Các Phương Pháp Quản Lý Trạng Thái Phổ Biến
2.1. Provider
Provider là một thư viện quản lý trạng thái đơn giản và mạnh mẽ, được khuyến nghị bởi chính nhóm phát triển Flutter. Nó giúp bạn quản lý trạng thái một cách hiệu quả mà không cần phải viết nhiều code boilerplate.
Cách Sử Dụng Provider
1. Thêm Provider vào Project:
Thêm thư viện provider vào file pubspec.yaml:
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
2. Tạo một Provider:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Thông báo cho các widget lắng nghe
}
}
3. Sử Dụng Provider trong Widget:
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Counter(),
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Provider Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<Counter>(
builder: (context, counter, child) {
return Text('Count: ${counter.count}');
},
),
ElevatedButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).increment();
},
child: Text('Increment'),
),
],
),
),
),
),
);
}
}
2.2. Riverpod
Riverpod là một phiên bản cải tiến của Provider, được thiết kế để khắc phục một số hạn chế của Provider. Riverpod cung cấp một cách tiếp cận linh hoạt và an toàn hơn để quản lý trạng thái.
1. Cách Sử Dụng Riverpod Thêm Riverpod vào Project: Thêm thư viện flutter_riverpod vào file pubspec.yaml:
dependencies: flutter: sdk: flutter flutter_riverpod: ^1.0.0
2. Tạo một Provider với Riverpod:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() {
state++;
}
}
3. Sử Dụng Riverpod trong Widget:
class CounterApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Riverpod Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: Text('Increment'),
),
],
),
),
),
);
}
}
2.3. BLoC (Business Logic Component)
1. BLoC là một mẫu kiến trúc giúp tách biệt logic nghiệp vụ (business logic) khỏi giao diện người dùng (UI). Nó sử dụng các luồng dữ liệu (streams) để quản lý trạng thái và cập nhật UI.
Cách Sử Dụng BLoC Thêm BLoC vào Project: Thêm thư viện flutter_bloc vào file pubspec.yaml:
dependencies: flutter: sdk: flutter flutter_bloc: ^8.0.0
2. Tạo một BLoC:
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
3. Sử Dụng BLoC trong Widget:
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CounterCubit(),
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('BLoC Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<CounterCubit, int>(
builder: (context, count) {
return Text('Count: $count');
},
),
ElevatedButton(
onPressed: () {
context.read<CounterCubit>().increment();
},
child: Text('Increment'),
),
],
),
),
),
),
);
}
}
3. So Sánh Các Phương Pháp Quản Lý Trạng Thái
| Phương Pháp | Ưu Điểm | Nhược Điểm |
|---|---|---|
| Provider | Đơn giản, dễ sử dụng, được khuyến nghị bởi Flutter. | Có thể trở nên phức tạp khi ứng dụng lớn hơn. |
| Riverpod | Linh hoạt, an toàn hơn Provider, không cần BuildContext. | Cần thời gian để làm quen với cú pháp mới. |
| BLoC | Tách biệt rõ ràng giữa logic và UI, phù hợp cho ứng dụng phức tạp. | Cần viết nhiều code boilerplate, khó tiếp cận cho người mới. |
4. Ví dụ code cho từng phương pháp
Cùng một bài toán — danh sách sản phẩm có tải/lỗi/dữ liệu — viết bằng ba cách.
Cách 1: Provider
class SanPhamProvider extends ChangeNotifier {
List<SanPham> sanPham = [];
bool dangTai = false;
String? loi;
Future<void> tai() async {
dangTai = true;
loi = null;
notifyListeners();
try {
sanPham = await api.laySanPham();
} catch (e) {
loi = e.toString();
} finally {
dangTai = false;
notifyListeners();
}
}
}
// Dùng trong widget
Consumer<SanPhamProvider>(
builder: (context, provider, _) {
if (provider.dangTai) return const CircularProgressIndicator();
if (provider.loi != null) return Text('Lỗi: ${provider.loi}');
return ListView(children: provider.sanPham.map(...).toList());
},
)
Ưu điểm: dễ hiểu nhất. Nhược điểm: notifyListeners() gọi ba lần → có thể rebuild thừa.
Cách 2: Riverpod
@riverpod
class DanhSachSanPham extends _$DanhSachSanPham {
@override
Future<List<SanPham>> build() async {
return api.laySanPham();
}
Future<void> lamMoi() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.laySanPham());
}
}
// Dùng trong widget
class ManHinhSanPham extends ConsumerWidget {
const ManHinhSanPham({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final async = ref.watch(danhSachSanPhamProvider);
return RefreshIndicator(
onRefresh: () => ref.read(danhSachSanPhamProvider.notifier).lamMoi(),
child: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Lỗi: $e')),
data: (ds) => ListView.builder(
itemCount: ds.length,
itemBuilder: (_, i) => ListTile(title: Text(ds[i].ten)),
),
),
);
}
}
Ưu điểm: AsyncValue buộc xử lý cả ba trạng thái; không cần BuildContext; test dễ hơn nhiều.
Cách 3: BLoC
// Events
abstract class SuKienSanPham {}
class TaiSanPham extends SuKienSanPham {}
// States
abstract class TrangThaiSanPham {}
class DangTai extends TrangThaiSanPham {}
class DaTai extends TrangThaiSanPham {
final List<SanPham> sanPham;
DaTai(this.sanPham);
}
class LoiTai extends TrangThaiSanPham {
final String thongDiep;
LoiTai(this.thongDiep);
}
// Bloc
class SanPhamBloc extends Bloc<SuKienSanPham, TrangThaiSanPham> {
final ApiClient api;
SanPhamBloc(this.api) : super(DangTai()) {
on<TaiSanPham>((event, emit) async {
emit(DangTai());
try {
emit(DaTai(await api.laySanPham()));
} catch (e) {
emit(LoiTai(e.toString()));
}
});
}
}
// Dùng trong widget
BlocBuilder<SanPhamBloc, TrangThaiSanPham>(
builder: (context, state) {
return switch (state) {
DangTai() => const CircularProgressIndicator(),
LoiTai(:final thongDiep) => Text('Lỗi: $thongDiep'),
DaTai(:final sanPham) => ListView.builder(
itemCount: sanPham.length,
itemBuilder: (_, i) => ListTile(title: Text(sanPham[i].ten)),
),
};
},
)
Ưu điểm: luồng dữ liệu một chiều, dễ trace trong app lớn. Nhược điểm: nhiều code cho tính năng đơn giản.
5. Nên chọn cái nào?
| Tình huống | Lựa chọn |
|---|---|
| Học Flutter, app nhỏ | Provider hoặc setState |
| App mới, dự án cá nhân | Riverpod |
| Team lớn, app nhiều màn hình phức tạp | BLoC |
| Công ty đã chuẩn hoá BLoC | BLoC |
| Cần test nhiều | Riverpod (dễ test nhất) |
💡 Lời khuyên thực tế: đừng chọn state management vì nó "trending". Hãy chọn theo quy mô app và quy ước của team. Chuyển đổi giữa các giải pháp tốn rất nhiều công — hãy quyết định sớm.
⚠️ Điều quan trọng hơn cả việc chọn thư viện: tách logic ra khỏi widget. Dù bạn dùng Provider, Riverpod hay BLoC, code vẫn tệ nếu bạn gọi API và xử lý nghiệp vụ ngay trong
build().