Chương 08 · Provider · Riverpod · GetX

Provider, Riverpod, GetX

Sau chương 7 (Bloc), chương này cover 3 alternative phổ biến. Mục tiêu không phải dạy hết, mà cho bạn đủ context để chọn + đọc code mở của người khác.

Thực tế ở VN: Provider (legacy), Riverpod (modern), GetX (legacy/team không senior), Bloc (enterprise). Hiểu cả 4 = đọc code ở bất kỳ team nào.

Độ dài: ~1110 dòng Bài tập: 5 Quiz: 8 Prerequisites: Ch 6, 7
🎯 Mục tiêu chương
  • 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

ProviderUse case
Provider<T>Immutable value (service, repository)
StateProvider<T>Simple mutable state (counter, toggle)
StateNotifierProviderComplex state với StateNotifier class (deprecated cho Notifier)
NotifierProviderModern — extends Notifier<T>
AsyncNotifierProviderAsync 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.
⚠️ GetX cho new code?

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')),
  ]);
}
🔀 Comparison table — 10 criteria
CriteriasetStateBlocProviderRiverpodGetX
BoilerplateMinHighMedMedLow
Type-safeRuntimeCompilePartial
Async first-class✓✓Partial
Test easeHard✓✓✓✓Hard
Popularity 2024HighMedHighDeclining
MaintainerFlutter teamFelix AngelovRemi R.Remi R.Jonny Borges
CodegenOptionalOptional
Learning curveLowHighLowMedLow
PerformanceGoodGoodGoodGoodGood
RecommendLocal stateEnterpriseBeginnerModernAvoid new code

5. Decision framework

🎯 Khi nào chọn cái nào?
  • 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: ChangeNotifierNotifier, ConsumerConsumerWidget hoặc Consumer widget, context.readref.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

Q1

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.

Q2

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.

Q3

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.

Q4

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.

Q5

Provider.value vs Provider?

Xem đáp án

Đáp án: Provider tạo mới (lifecycle managed). Provider.value reuse existing instance, không dispose.

Q6

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.

Q7

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.

Q8

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.