- Provider: foundational wrapper trên InheritedWidget + ChangeNotifier.
- Riverpod: Provider 2.0 — compile-time safe, không cần BuildContext.
- GetX: all-in-one (state, navigation, DI, i18n) — controversial.
- Hiểu tradeoff mỗi thư viện.
- Migrate Counter qua 4 cách: setState, Provider, Riverpod, GetX.
- Recommendation: khi nào chọn cái nào.
1. Provider — foundation
provider package là officially recommended by Flutter team trước khi Riverpod ra đời.
Wrapper trên InheritedWidget + ChangeNotifier.
flutter pub add provider
import 'package:provider/provider.dart';
// Model
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// Provide
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
)
// Consume — 2 cách
// Cách 1: Consumer
Consumer<CounterModel>(
builder: (context, model, child) => Text('${model.count}'),
)
// Cách 2: context shortcut
final count = context.watch<CounterModel>().count;
Text('$count')
Helper methods
context.read<T>()— lấy ref, no rebuild. Cho callback.context.watch<T>()— subscribe, rebuild. Cho build().context.select<T, R>(selector)— rebuild chỉ khi selector output đổi.
MultiProvider
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthModel()),
ChangeNotifierProvider(create: (_) => CartModel()),
Provider(create: (_) => ApiClient()), // non-Listenable, immutable
],
child: MyApp(),
)
Provider pros / cons
- ✅ Pros: simple, official, ít magic, foundation cho hầu hết thư viện khác.
- ❌ Cons: cần BuildContext (không gọi từ pure Dart), runtime error nếu Provider không tồn tại trong tree.
2. Riverpod — Provider 2.0
Cùng author (Remi Rousselet). Khắc phục weakness Provider:
- Compile-time safe — provider không tồn tại → compile error.
- Không cần BuildContext — access qua
ref(WidgetRef hoặc Ref). - Test dễ — override provider trong test.
flutter pub add flutter_riverpod
Counter
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Global declaration — top-level
final counterProvider = StateProvider<int>((ref) => 0);
// Wrap app
void main() {
runApp(const ProviderScope(child: MyApp()));
}
// Use trong widget — extend ConsumerWidget
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
Các loại Provider Riverpod
| Provider | Use case |
|---|---|
Provider<T> | Immutable value (service, repository) |
StateProvider<T> | Simple mutable state (counter, toggle) |
StateNotifierProvider | Complex state với StateNotifier class (deprecated cho Notifier) |
NotifierProvider | Modern — extends Notifier<T> |
AsyncNotifierProvider | Async state, AsyncValue |
FutureProvider<T> | One-shot async value (fetch lần đầu) |
StreamProvider<T> | Stream value |
Async với FutureProvider
final userProvider = FutureProvider<User>((ref) async {
final api = ref.read(apiProvider);
return api.fetchCurrentUser();
});
// Use
class UserPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncUser = ref.watch(userProvider);
return asyncUser.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (e, st) => Text('Error: $e'),
);
}
}
family + autoDispose
// family — provider parameterized
final userProvider = FutureProvider.family<User, int>((ref, id) async {
return ref.read(apiProvider).fetchUser(id);
});
// Use
ref.watch(userProvider(42));
// autoDispose — dispose khi không ai watch
final tempCounterProvider = StateProvider.autoDispose<int>((ref) => 0);
// Combine
final p = FutureProvider.family.autoDispose<User, int>((ref, id) async {/*...*/});
Riverpod pros / cons
- ✅ Pros: compile-time safe, async first-class (
AsyncValue.when), không context. - ❌ Cons: learning curve cao hơn Provider, codegen optional làm phức tạp setup.
3. GetX — all-in-one
GetX bao gồm state, navigation, DI, i18n, snackbar — 1 package làm hết.
flutter pub add get
Counter
import 'package:get/get.dart';
class CounterController extends GetxController {
var count = 0.obs; // RxInt — reactive
void increment() => count++;
}
// Use trong widget
class CounterPage extends StatelessWidget {
final ctrl = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
body: Obx(() => Text('${ctrl.count}')),
floatingActionButton: FloatingActionButton(
onPressed: ctrl.increment,
child: const Icon(Icons.add),
),
);
}
}
GetX navigation — không cần context
Get.to(() => DetailPage());
Get.back();
Get.toNamed('/detail');
Get.snackbar('Title', 'Message');
Get.dialog(AlertDialog(/*...*/));
GetX pros / cons
- ✅ Pros: ít boilerplate, không cần context, all-in-one.
- ❌ Cons:
- Maintainer history controversial (breaking change frequent, design choice argue).
- Navigation không context = anti-pattern Flutter — hard to test.
- Magic — code khó trace, debug.
Tránh GetX cho code mới năm 2024+. Maintainer track record có vấn đề, ecosystem fracture. Nếu inherit legacy GetX project, vẫn tiếp tục được; nhưng greenfield → chọn Bloc / Riverpod.
4. Counter migration — 4 cách
Cùng app Counter, 4 implementation. Side-by-side để cảm nhận:
setState (Ch 2)
class _CounterState extends State<Counter> {
int _c = 0;
@override
Widget build(BuildContext context) => Column(
children: [
Text('$_c'),
ElevatedButton(onPressed: () => setState(() => _c++), child: const Text('+1')),
],
);
}
Bloc (Ch 7)
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void inc() => emit(state + 1);
}
BlocProvider(
create: (_) => CounterCubit(),
child: BlocBuilder<CounterCubit, int>(
builder: (ctx, c) => Column(children: [
Text('$c'),
ElevatedButton(onPressed: ctx.read<CounterCubit>().inc, child: const Text('+1')),
]),
),
)
Provider
class CounterModel extends ChangeNotifier {
int count = 0;
void inc() { count++; notifyListeners(); }
}
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: Consumer<CounterModel>(
builder: (ctx, m, _) => Column(children: [
Text('${m.count}'),
ElevatedButton(onPressed: m.inc, child: const Text('+1')),
]),
),
)
Riverpod
final counterProvider = StateProvider<int>((ref) => 0);
class Counter extends ConsumerWidget {
@override
Widget build(BuildContext ctx, WidgetRef ref) => Column(children: [
Text('${ref.watch(counterProvider)}'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Text('+1'),
),
]);
}
GetX
class CounterCtrl extends GetxController {
var count = 0.obs;
void inc() => count++;
}
class Counter extends StatelessWidget {
final c = Get.put(CounterCtrl());
@override
Widget build(BuildContext _) => Column(children: [
Obx(() => Text('${c.count}')),
ElevatedButton(onPressed: c.inc, child: const Text('+1')),
]);
}
| Criteria | setState | Bloc | Provider | Riverpod | GetX |
|---|---|---|---|---|---|
| Boilerplate | Min | High | Med | Med | Low |
| Type-safe | ✓ | ✓ | Runtime | Compile | Partial |
| Async first-class | — | ✓ | — | ✓✓ | Partial |
| Test ease | Hard | ✓✓ | ✓ | ✓✓ | Hard |
| Popularity 2024 | — | High | Med | High | Declining |
| Maintainer | Flutter team | Felix Angelov | Remi R. | Remi R. | Jonny Borges |
| Codegen | — | Optional | — | Optional | — |
| Learning curve | Low | High | Low | Med | Low |
| Performance | Good | Good | Good | Good | Good |
| Recommend | Local state | Enterprise | Beginner | Modern | Avoid new code |
5. Decision framework
- Project nhỏ / prototype / học: setState + Provider.
- Team có Bloc experience: Bloc.
- Team modern + safety concern: Riverpod (recommended cho new project 2024+).
- Tránh: GetX cho greenfield (maintainer reliability).
- Legacy GetX project: tiếp tục, không migrate vội — cost > benefit.
Trong sub-pillar IT Basic này, các chương sau sẽ dùng Bloc/Cubit làm chính (đã học chương 7).
6. Đọc code mở
OSS Flutter apps để học pattern:
- Wonderous (github.com/gskinnerTeam/flutter-wonderous-app) — gskinner, không state mgmt lib (Provider light).
- Flutter Samples (github.com/flutter/samples) — Flutter team examples.
- InvenTree — open source inventory app, dùng Provider.
- Reflectly (legacy) — dùng Bloc.
7. Bài tập
Counter với Provider
Counter app với Provider. Add reset button. Test với widget test.
💡 Gợi ý đáp án
Tham khảo section 1 (Counter Provider) + Ch 14 widget test (sẽ học sau).
Migrate Provider → Riverpod
Migrate bài 1 từ Provider sang Riverpod. Đếm số dòng code thay đổi.
💡 Gợi ý đáp án
Provider → Riverpod thay đổi chính: ChangeNotifier → Notifier, Consumer → ConsumerWidget hoặc Consumer widget, context.read → ref.read.
Migrate sang GetX
Migrate sang GetX. So sánh syntax với 2 cách trước.
💡 Gợi ý đáp án
GetX dùng .obs + Obx wrapper. Get.put register instance.
Đọc OSS app
Đọc code mở 1 OSS Flutter app (vd wonderous_app). Identify state management dùng. Mô tả pattern.
💡 Gợi ý đáp án
Wonderous dùng Provider light. Có một AppLogic singleton (extends ChangeNotifier) cung cấp toàn app state.
Chọn library cho use case
Cho use case "shopping cart cross-screen", chọn thư viện phù hợp + giải thích reasoning.
💡 Gợi ý đáp án
Bloc nếu team enterprise — sealed CartState (Loading, Loaded, Error), CartEvent. Riverpod nếu team modern —
NotifierProvider<CartNotifier, CartState>. Provider nếu team beginner — ChangeNotifierProvider<CartModel>.
8. Quiz
Provider vs Riverpod — khác chính?
Xem đáp án
Đáp án: Riverpod compile-time safe (provider không tồn tại → compile error). Provider runtime error. Riverpod không cần BuildContext.
GetX maintainer history controversial — vấn đề gì?
Xem đáp án
Đáp án: Breaking change thường, semver lỏng, design choice argue (vd navigation không context). Avoid for new code.
context.watch<T>() vs Consumer<T>?
Xem đáp án
Đáp án: watch rebuild whole widget. Consumer rebuild chỉ subtree trong builder — perf hint khi widget lớn.
Riverpod autoDispose làm gì?
Xem đáp án
Đáp án: Dispose provider khi không widget nào watch. Tránh memory leak khi state liên quan page tạm.
Provider.value vs Provider?
Xem đáp án
Đáp án: Provider tạo mới (lifecycle managed). Provider.value reuse existing instance, không dispose.
GetX Get.put(Controller()) rồi Get.find<Controller>() — pattern gì?
Xem đáp án
Đáp án: Service locator / DI. Tương tự package get_it. GetX có DI built-in.
Riverpod family modifier — use case?
Xem đáp án
Đáp án: Parameterized provider: userProvider.family<User, int>((ref, id) => fetchUser(id)). Mỗi id → instance riêng cache.
Tại sao có nhiều thư viện state management?
Xem đáp án
Đáp án: Khác philosophy: explicit vs ergonomic, type-safety vs simplicity, separation of concerns vs all-in-one. Mỗi team trade-off khác. Cùng build trên InheritedWidget primitive.
9. Tổng kết
- ✅ Provider: foundation, ChangeNotifier + InheritedWidget wrapper.
- ✅ Riverpod: compile-time safe Provider, không cần context, AsyncValue cho async.
- ✅ GetX: all-in-one — state + nav + DI + i18n. Controversial cho new code.
- ✅ Counter migration 4 cách side-by-side — feel boilerplate vs ergonomic.
- ✅ Decision framework: project nhỏ → Provider; modern → Riverpod; enterprise → Bloc; tránh GetX greenfield.
- ✅ Đọc code mở để học pattern thực tế.
10. Kết nối
- Ch 6, 7: foundation + Bloc.
- Ch 10 — Network: state mgmt gọi repo.
- Ch 14 — Testing: test với mỗi state lib.