Riverpod là một thư viện quản lý state cho Flutter, được xây dựng như “phiên bản nâng cấp” của Provider: an toàn hơn, linh hoạt hơn, dễ test hơn và ít “hack” hơn. Nếu đang xây app Flutter trung bình đến lớn, Riverpod là một lựa chọn rất đáng để chuẩn hoá.
1. Riverpod là gì?
Riverpod là một thư viện quản lý state cho Flutter (và Dart nói chung), được tạo ra bởi chính tác giả của Provider (Rémi Rousselet). Mục tiêu của Riverpod:
- Loại bỏ các hạn chế và “góc chết” của Provider.
- Đem lại mô hình quản lý state:
- An toàn compile-time (bắt lỗi ngay khi code).
- Dễ test vì state tách biệt với widget tree.
- Linh hoạt: dùng được cho cả app nhỏ lẫn lớn, cho cả logic thuần Dart.
Riverpod không phụ thuộc vào widget tree như Provider; thay vào đó nó dùng một “container” để quản lý và cung cấp state. Điều này giúp:
- Không cần
BuildContextđể đọc provider. - Có thể dùng trong pure Dart (VD: trong isolate, background, hay script).
2. Tại sao Riverpod ra đời (và vượt trội hơn Provider)?
Hạn chế của Provider
Khi dùng Provider lâu dài, thường gặp những vấn đề:
- Phụ thuộc vào BuildContext
Mọi thứ gắn chặt vào widget tree:- Đọc provider phải có
BuildContext. - Di chuyển widget, refactor tree có thể gây lỗi “Provider not found”.
- Đọc provider phải có
- Dễ gặp lỗi runtime hơn là compile-time
Ví dụ: gọiProvider.of<T>sai kiểu, đọc provider ở context không phù hợp, hoặc quên đăng ký provider ở trên trong tree. - Khó test logic thuần Dart
Vì state gắn liền với widget tree, test thường phải dựng cả widget (WidgetTest) dù chỉ muốn test logic.
Riverpod giải quyết như thế nào?
- Không cần BuildContext để đọc provider
Với Riverpod (đặc biệt làriverpodhoặcflutter_riverpod), logic có thể đọc provider thông quaref, không nhất thiết phải có context của widget. - An toàn kiểu (type-safety) và compile-time hơn
Riverpod sử dụng generics và cơ chế provider rõ ràng, giúp IDE và compiler bắt lỗi sớm. - Dễ test
Chỉ cần tạoProviderContainervà đọc/ghi state y như trong runtime, không cần build widget. - Không còn “magic” dựa trên InheritedWidget
Riverpod “định nghĩa lại” cách quản lý state dựa trên container, rõ ràng và thuận tiện hơn trong những kiến trúc phức tạp.
3. Các loại Provider trong Riverpod
Riverpod có nhiều loại provider, mỗi loại phù hợp với một dạng state:
3.1. Provider – giá trị chỉ đọc (read-only)
Dùng cho những giá trị bất biến hoặc tính toán được từ nơi khác, không thay đổi theo thời gian.
dartfinal appNameProvider = Provider<String>((ref) {
return 'My Awesome App';
});
Trong widget (Flutter):
dartclass HomeTitle extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appName = ref.watch(appNameProvider);
return Text(appName);
}
}
3.2. StateProvider – state đơn giản, mutable
Dùng cho state đơn giản kiểu int, bool, String, hoặc các model nhỏ, tương tự ValueNotifier.
dartfinal counterProvider = StateProvider<int>((ref) => 0);
Sử dụng trong widget:
dartclass CounterView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Text('Increment'),
),
],
);
}
}
3.3. FutureProvider – cho dữ liệu bất đồng bộ (future)
Thích hợp khi cần load dữ liệu từ API hoặc local DB (một lần hoặc thỉnh thoảng).
dartfinal userFutureProvider = FutureProvider<User>((ref) async {
// Gọi API, ví dụ:
final user = await fetchUserFromApi();
return user;
});
Trong UI:
dartclass UserProfile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncUser = ref.watch(userFutureProvider);
return asyncUser.when(
data: (user) => Text('Hello, ${user.name}'),
loading: () => const CircularProgressIndicator(),
error: (e, st) => Text('Error: $e'),
);
}
}
3.4. StreamProvider – stream liên tục
Dùng với stream (WebSocket, Firestore, event bus,…):
dartfinal messagesStreamProvider = StreamProvider<List<Message>>((ref) {
return listenMessagesStream(); // Stream<List<Message>>
});
3.5. StateNotifierProvider – state phức tạp, business logic rõ ràng
Khi state trở nên phức tạp (nhiều field, nhiều hành động), nên dùng StateNotifier. Đây là mô hình được dùng rất nhiều trong các kiến trúc clean architecture.
dart// 1. Định nghĩa state
class Todo {
final String id;
final String title;
final bool completed;
Todo({required this.id, required this.title, this.completed = false});
Todo copyWith({String? title, bool? completed}) {
return Todo(
id: id,
title: title ?? this.title,
completed: completed ?? this.completed,
);
}
}
// 2. Định nghĩa StateNotifier
class TodoListNotifier extends StateNotifier<List<Todo>> {
TodoListNotifier() : super([]);
void addTodo(String title) {
final newTodo = Todo(id: DateTime.now().toIso8601String(), title: title);
state = [...state, newTodo];
}
void toggle(String id) {
state = [
for (final todo in state)
if (todo.id == id)
todo.copyWith(completed: !todo.completed)
else
todo,
];
}
}
// 3. Tạo provider
final todoListProvider =
StateNotifierProvider<TodoListNotifier, List<Todo>>((ref) {
return TodoListNotifier();
});
Sử dụng trong widget:
dartclass TodoListView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
title: Text(todo.title),
value: todo.completed,
onChanged: (_) {
ref.read(todoListProvider.notifier).toggle(todo.id);
},
),
],
);
}
}
4. Cách dùng Riverpod trong Flutter
4.1. Cài đặt
Thêm dependency vào pubspec.yaml:
textdependencies:
flutter_riverpod: ^2.0.0 # (hoặc phiên bản mới hơn)
4.2. Khởi tạo ở root
Bọc app bằng ProviderScope:
dartvoid main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
ProviderScope là nơi chứa “container” của Riverpod – nó quản lý toàn bộ provider trong app.
4.3. Đọc provider trong widget
Có 3 pattern chính:
- ConsumerWidget (thường dùng nhất)
dartclass MyHomePage extends ConsumerWidget {
@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),
),
);
}
}
- Consumer (khi không muốn đổi class cha)
dartclass MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, _) {
final count = ref.watch(counterProvider);
return Text('$count');
},
);
}
}
- HookConsumerWidget (kết hợp với flutter_hooks, nếu thích hooks)
dartclass MyHomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
5. So sánh Riverpod với các thư viện state management khác
Riverpod vs Provider
| Tiêu chí | Provider | Riverpod |
|---|---|---|
| Phụ thuộc BuildContext | Có | Không (dùng ref) |
| An toàn compile-time | Tốt nhưng vẫn có lỗ hổng | Tốt hơn, nhiều lỗi bị bắt khi compile |
| Test logic | Cần widget test | Dùng ProviderContainer test thuần Dart |
| Khả năng tái sử dụng | Phụ thuộc widget tree | Logic thuần Dart, dễ tái sử dụng, plugin lại kiến trúc clean |
| API & tổ chức code | Dễ đơn giản nhưng rối khi phức tạp | Rõ ràng, phân tách tốt giữa state và widget |
Riverpod vs Bloc/Cubit
- Bloc rất mạnh về kiến trúc hướng event/state, pattern rõ ràng, phù hợp team muốn tách biệt logic rõ.
- Riverpod linh hoạt hơn về kiểu provider, có thể implement BLoC trên Riverpod luôn.
- Riverpod thường ít boilerplate hơn, dễ bắt đầu hơn cho developer mới.
Riverpod vs GetX / MobX / Redux
- GetX: nhanh, nhiều thứ built-in (routing, DI, snackBar…) nhưng bị phê bình về mặt “opinionated” và có thể khó maintain dài hạn.
- MobX: reactive “magic”, code khá đẹp nhưng đôi khi khó debug luồng.
- Redux: rất rõ ràng, pattern chuẩn, nhưng boilerplate lớn, “nặng” cho app nhỏ/vừa.
Tổng quan:
Riverpod nằm ở “điểm cân bằng” tốt giữa:
- Cấu trúc rõ ràng.
- Boilerplate vừa phải.
- Linh hoạt, dễ mở rộng, dễ test.
6. Tư duy kiến trúc với Riverpod
Để dùng Riverpod hiệu quả, nên tổ chức code theo các tầng:
- Tầng data (repository, datasource)
Không biết gì về UI, chỉ là class/service thuần Dart.
Có thể expose quaProviderhoặcProvider.autoDispose. - Tầng logic (StateNotifier / Notifier)
Chứa business logic:- Gọi repository.
- Xử lý, validate, map dữ liệu.
- Cập nhật state.
- Tầng UI (Widget)
Chỉ:ref.watchđể lấy state.ref.read(...).method()để gọi action.
Ví dụ đơn giản:
dart// Repository
class AuthRepository {
Future<User?> login(String email, String password) async {
// call API...
}
}
// Provider repository
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepository();
});
// State
class AuthState {
final bool isLoading;
final User? user;
final String? error;
AuthState({this.isLoading = false, this.user, this.error});
AuthState copyWith({bool? isLoading, User? user, String? error}) {
return AuthState(
isLoading: isLoading ?? this.isLoading,
user: user ?? this.user,
error: error,
);
}
}
// Notifier
class AuthNotifier extends StateNotifier<AuthState> {
final AuthRepository _repo;
AuthNotifier(this._repo) : super(AuthState());
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true, error: null);
try {
final user = await _repo.login(email, password);
if (user == null) {
state = state.copyWith(isLoading: false, error: 'Login failed');
} else {
state = state.copyWith(isLoading: false, user: user);
}
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
}
// Provider cho AuthNotifier
final authProvider =
StateNotifierProvider<AuthNotifier, AuthState>((ref) {
final repo = ref.watch(authRepositoryProvider);
return AuthNotifier(repo);
});
Trong UI:
dartclass LoginButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
return ElevatedButton(
onPressed: authState.isLoading
? null
: () {
ref.read(authProvider.notifier).login('email@test.com', '123456');
},
child: authState.isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Login'),
);
}
}
7. Một số tính năng mạnh mẽ khác của Riverpod
7.1. autoDispose
Khi muốn provider tự động giải phóng khi không còn được sử dụng (giải phóng memory, cancel stream, v.v.):
dartfinal searchResultProvider = FutureProvider.autoDispose<List<Item>>((ref) async {
// logic call API search
});
Tiện cho:
- Màn hình search.
- Màn hình tạm thời, không cần giữ state lâu dài.
7.2. Override provider (testing, multi môi trường)
Nhờ cơ chế override, rất dễ test hoặc chuyển môi trường (dev/stage/prod).
dartvoid main() {
test('AuthNotifier login success', () async {
final container = ProviderContainer(
overrides: [
authRepositoryProvider.overrideWithValue(FakeAuthRepositorySuccess()),
],
);
final notifier = container.read(authProvider.notifier);
await notifier.login('test@test.com', '123');
expect(container.read(authProvider).user, isNotNull);
});
}
Tương tự, có thể override API endpoint, fake repo khi chạy trong dev mode, v.v.
7.3. Riverpod Generator (riverpod_annotation)
Để giảm boilerplate, Riverpod hỗ trợ code generation, dùng annotation để định nghĩa provider một cách ngắn gọn. Đây là bước nâng cao, thích hợp khi đã quen với Riverpod cơ bản.
8. Khi nào nên chọn Riverpod cho dự án?
Riverpod phù hợp trong các trường hợp:
- App trung bình đến lớn, có nhiều màn hình, nhiều loại state.
- Team muốn:
- Tách biệt rõ giữa UI và business logic.
- Dễ test logic thuần Dart.
- Kiến trúc sạch (clean architecture, layered architecture).
- Dự án dùng Provider nhưng bắt đầu gặp hạn chế:
- Khó refactor.
- Khó test.
- Context bị “vướng” lung tung.
- Muốn một giải pháp ổn định, được cộng đồng support mạnh, không quá “opinionated” như một số framework khác.
9. Nhược điểm và đường cong học tập
Không có thư viện nào hoàn hảo, Riverpod cũng vậy:
- Đường cong học tập
So với Provider đơn thuần, Riverpod có nhiều khái niệm hơn (ref, container, nhiều loại provider). Ban đầu cảm giác “nặng” với app nhỏ. - Có nhiều cách để làm cùng một việc
Vừa là điểm mạnh vừa là điểm yếu: dễ phân vân chọnStateProviderhayStateNotifierProvider,FutureProviderhay tự quản lý trong Notifier,… - Cần thống nhất convention trong team
Nếu không quy ước từ đầu (folder structure, naming, khi nào dùng loại provider nào), codebase dễ trở nên lộn xộn.
10. Kết luận
Riverpod không chỉ là “Provider v2” mà là một cách tiếp cận mới, hiện đại hơn để quản lý state trong Flutter:
- An toàn, dễ test, dễ mở rộng
Tách logic khỏi UI, tận dụng tốt Dart thuần. - Linh hoạt
Hỗ trợ nhiều kiểu state: sync, async, stream, logic phức tạp bằng StateNotifier hoặc Notifier. - Phù hợp cả dự án vừa và lớn
Khi codebase tăng dần, lợi thế của Riverpod càng rõ rệt.
Nếu đang bắt đầu một dự án Flutter mới hoặc muốn refactor hệ thống state management cũ, Riverpod là một lựa chọn đáng cân nhắc để đặt làm “xương sống” cho kiến trúc ứng dụng.
Trong các bài viết tiếp theo, có thể đào sâu thêm từng chủ đề:
- Cách tổ chức folder & kiến trúc với Riverpod (presentation / application / domain / data).
- Sử dụng Riverpod Generator để giảm boilerplate.
- Best practices: error handling, logging, performance với Riverpod.
Để lại một bình luận