Riverpod Flutter: Hướng Dẫn Toàn Diện Từ Cơ Bản Đến Nâng Cao (Cập Nhật 2026)
Nếu bạn đã từng dùng Provider trong Flutter, chắc hẳn bạn cũng từng gặp những rắc rối như ProviderNotFoundException, dependency vào BuildContext hay phải bọc MultiProvider rườm rà. Đây chính là lý do Riverpod ra đời.
Riverpod được tạo ra bởi chính tác giả của Provider – Rémi Rousselet, với mục tiêu khắc phục tất cả những hạn chế đó nhưng vẫn giữ nguyên phong cách reactive, dễ dùng. Trong bài viết này, chúng ta sẽ cùng nhau làm chủ Riverpod từ cơ bản đến nâng cao.
1. Tại sao nên dùng Riverpod thay vì Provider?
1.1. Provider còn hạn chế gì?
Provider hoạt động dựa trên widget tree (cây giao diện). Nghĩa là để cung cấp một đối tượng, bạn phải bọc nó trong MultiProvider, và khi muốn lấy dữ liệu, bạn bắt buộc phải có BuildContext.
- Dễ crash runtime: Quên không khai báo
ChangeNotifierProviderđúng cấp độ →ProviderNotFoundExceptionkhi chạy. - Khó kiểm soát khi có hai provider cùng kiểu: Provider chỉ biết phân biệt bằng kiểu dữ liệu (type), nếu bạn có hai
Stringkhác nhau sẽ bị đè lên nhau. - Phụ thuộc Flutter: Bạn không thể dùng Provider trong phần logic thuần Dart hay CLI tool.
1.2. Riverpod giải quyết những vấn đề đó thế nào?
Riverpod giải quyết triệt để các hạn chế trên, theo tài liệu chính thức:
- Compile-safe (An toàn kiểu khi biên dịch): Nếu code chạy được, nó sẽ hoạt động. Không còn lỗi
ProviderNotFoundExceptionkhi runtime như Provider nữa. - Không phụ thuộc widget tree: Provider trong Riverpod là các biến toàn cục, bạn có thể gọi ở bất kỳ đâu, kể cả ngoài Flutter, mà không cần
BuildContext. - Không bị nhầm lẫn type: Trong Riverpod, mỗi provider là một đối tượng riêng biệt. Nếu cần hai provider cùng trả về
String, bạn khai báo hai biến khác nhau:final userNameProvidervàfinal apiUrlProvider– hoàn toàn độc lập. - Hỗ trợ async tuyệt vời:
FutureProvidervàAsyncNotifierxử lý loading/error/data mặc định, không cần dùngFutureBuilderthủ công.
2. Cài đặt Riverpod vào project Flutter
Kiểm chứng phiên bản: Truy cập pub.dev/packages/flutter_riverpod để lấy phiên bản mới nhất. Hiện tại (2025) phiên bản ổn định là ^2.6.1.
Trước tiên, bạn cần thêm package flutter_riverpod – đây là package dành cho Flutter, chứ không phải riverpod core (dùng cho Dart CLI). Một lỗi rất phổ biến là nhầm giữa hai package này.
2.1. Thêm dependency vào pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.6.1 # Kiểm tra phiên bản mới nhất trên pub.dev
riverpod_annotation: ^4.3.3 # optional - dùng cho code generation
dev_dependencies:
build_runner: ^2.4.14
riverpod_generator: ^4.3.0
Trong phạm vi bài viết này, chúng ta sẽ sử dụng cách thủ công (không code generation) để dễ hình dung. Khi đã quen, bạn có thể tham khảo thêm riverpod_generator để viết code ngắn gọn hơn.
2.2. Chạy flutter pub get
Mở terminal và chạy:
flutter pub get
2.3. Wrap MaterialApp với ProviderScope
Mọi ứng dụng Flutter dùng Riverpod bắt buộc phải có ProviderScope ở đầu cây widget. Nó giống như bộ nhớ chứa tất cả các provider và state.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope( // 👈 BẮT BUỘC: Bọc ProviderScope
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Demo',
home: const HomePage(),
);
}
}
3. Provider cơ bản: tạo và đọc giá trị
3.1. Provider (dành cho giá trị immutable – chỉ đọc)
Provider dùng để expose một object không thay đổi, hoặc dependency injection (DI). Đây là provider dễ nhất.
// Định nghĩa provider - là top-level variable
final greetingProvider = Provider
<String>((ref) {
return 'Chào bạn, đây là Riverpod!';
});
Để đọc giá trị và hiển thị lên UI:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class GreetingWidget extends ConsumerWidget { // 👈 Dùng ConsumerWidget thay vì StatelessWidget
const GreetingWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) { // 👈 Có thêm tham số WidgetRef ref
final greeting = ref.watch(greetingProvider);
return Text(greeting);
}
}
Giải thích:
ref.watch(greetingProvider): đọc giá trị từ provider và lắng nghe sự thay đổi. Khi greetingProvider thay đổi, widget này tự rebuild.
ConsumerWidget: là dạng StatelessWidget có thêm WidgetRef để tương tác với các provider.
3.2. StateProvider (dành cho state đơn giản, mutable)
StateProvider dùng cho các state đơn giản như counter, checkbox, text field. Nó cung cấp cả giá trị lẫn cách thay đổi.
final counterProvider = StateProvider
<int>((ref) {
return 0; // initial state
});
class CounterWidget extends ConsumerWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // đọc giá trị
// Lấy notifier để gọi thay đổi state
final counterNotifier = ref.read(counterProvider.notifier);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () {
// Cách an toàn: dùng update thay vì gán state trực tiếp
counterNotifier.update((value) => value + 1);
},
child: const Text('Increment'),
),
],
);
}
}
Lưu ý: StateProvider chỉ nên dùng khi state đơn giản (int, bool, String). Nếu state có nhiều field hoặc có logic phức tạp, hãy dùng NotifierProvider (phần 4).
3.3. Cách đọc: ref.watch, ref.read, ref.listen
ref.watch: dùng trong build method (của widget hoặc provider). Khi state thay đổi, widget/provider rebuild. Đây là cách dùng chính.ref.read: dùng trong event handler, callback, initState để đọc giá trị một lần mà không rebuild. Riverpod 2.x không khuyến khích dùngref.readtrong build method vì sẽ không rebuild khi state thay đổi.ref.listen: dùng để thực thi side effect (hiển thị SnackBar, ghi log) khi state thay đổi, mà không rebuild UI.
ref.listen
<String>(someProvider, (previous, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Giá trị thay đổi thành $next')),
);
});
4. Xử lý state phức tạp với NotifierProvider
Khi state có cấu trúc phức tạp (list, object) và cần các method để thay đổi state, không dùng StateProvider. Hãy dùng NotifierProvider. Đây là cách được Riverpod khuyến khích cho phần lớn ứng dụng thực tế.
4.1. Tạo Notifier class
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Định nghĩa model Todo (đơn giản)
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 Notifier class
class TodoNotifier extends Notifier<List<Todo>> {
@override
List
<Todo> build() {
// Khởi tạo state ban đầu - nếu có logic bất đồng bộ, dùng AsyncNotifier
return [];
}
// Phương thức thêm todo
void addTodo(Todo todo) {
// State là immutable, không được push trực tiếp.
// Tạo mảng mới bằng spread operator
state = [...state, todo];
}
// Phương thức xoá todo
void removeTodo(String id) {
state = [for (final todo in state) if (todo.id != id) todo];
}
// Phương thức toggle complete
void toggleTodo(String id) {
state = [
for (final todo in state)
if (todo.id == id)
todo.copyWith(completed: !todo.completed)
else
todo
];
}
}
// 3. Định nghĩa NotifierProvider
final todoProvider = NotifierProvider<TodoNotifier, List<Todo>>(
TodoNotifier.new,
);
Giải thích:
Notifier là class chứa logic state và các method thay đổi state. Khác với ChangeNotifier trong Provider, bạn không gọi notifyListeners(). Chỉ cần gán state = ... là Riverpod tự biết để rebuild UI.
- State bắt buộc phải immutable. Trong Dart, sử dụng spread operator
[...state] hoặc copyWith để tạo bản sao mới khi thay đổi.
4.2. Gọi method thay vì gán trực tiếp state
Trong UI, bạn có thể gọi trực tiếp các method của TodoNotifier:
class TodoListWidget extends ConsumerWidget {
const TodoListWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoProvider);
// Lấy notifier để gọi method
final todoNotifier = ref.read(todoProvider.notifier);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
leading: Checkbox(
value: todo.completed,
onChanged: (_) => todoNotifier.toggleTodo(todo.id),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => todoNotifier.removeTodo(todo.id),
),
);
},
);
}
}
4.3. Ví dụ: Todo list với add/remove/toggle (tiếp tục)
Thêm một TextField để nhập todo, chú ý dispose controller:
class AddTodoWidget extends ConsumerStatefulWidget {
const AddTodoWidget({super.key});
@override
ConsumerState
<AddTodoWidget> createState() => _AddTodoWidgetState();
}
class _AddTodoWidgetState extends ConsumerState
<AddTodoWidget> {
final TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose(); // 👈 KHÔNG BAO GIỜ QUÊN DISPOSE
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(controller: controller),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
final todoNotifier = ref.read(todoProvider.notifier);
if (controller.text.trim().isNotEmpty) {
todoNotifier.addTodo(
Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: controller.text,
),
);
controller.clear();
}
},
),
],
);
}
}
Lưu ý quan trọng: Khi cần gọi method của NotifierProvider từ event (onPressed, initState), hãy dùng ref.read(todoProvider.notifier). Không dùng ref.watch ở đây vì bạn không cần rebuild, chỉ cần lấy notifier để gọi hàm.
5. Gọi API async với FutureProvider và AsyncNotifier
Xử lý bất đồng bộ (gọi API, đọc file, truy vấn database) là nhu cầu phổ biến. Riverpod cung cấp hai công cụ mạnh mẽ: FutureProvider cho các tác vụ đơn giản và AsyncNotifier cho các tác vụ phức tạp cần refresh hoặc method riêng.
5.1. FutureProvider đơn giản
FutureProvider tự động quản lý trạng thái loading, error, data.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
// 1. Định nghĩa model
class Post {
final int id;
final String title;
Post({required this.id, required this.title});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(id: json['id'], title: json['title']);
}
}
// 2. Định nghĩa FutureProvider
final postsProvider = FutureProvider<List<Post>>((ref) async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode != 200) throw Exception('Failed to load posts');
final List
<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
});
Sử dụng trong UI với Consumer hoặc ConsumerWidget:
class PostsWidget extends ConsumerWidget {
const PostsWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final postsAsync = ref.watch(postsProvider);
// AsyncValue có sẵn method .when để xử lý 3 trạng thái
return postsAsync.when(
data: (posts) => ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) => ListTile(title: Text(posts[index].title)),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
);
}
}
5.2. AsyncNotifier cho phép refresh
Nếu bạn cần làm mới dữ liệu (refresh) hoặc có các method thay đổi dữ liệu từ API, hãy dùng AsyncNotifier. AsyncNotifier vừa quản lý state bất đồng bộ, vừa có thể chứa các method để tương tác với API.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class PostsNotifier extends AsyncNotifier<List<Post>> {
// Hàm build được gọi một lần khi provider khởi tạo, có thể async
@override
Future<List<Post>> build() async {
return await _fetchPosts();
}
Future<List<Post>> _fetchPosts() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode != 200) throw Exception('Failed to load posts');
final List
<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
}
// Phương thức refresh, gọi từ UI
Future
<void> refresh() async {
// AsyncNotifier tự quản lý loading state trong guard
state = await AsyncValue.guard(() => _fetchPosts());
}
}
final postsProvider2 = AsyncNotifierProvider<PostsNotifier, List<Post>>(
PostsNotifier.new,
);
UI có thể gọi refresh:
ref.read(postsProvider2.notifier).refresh();
5.3. UI hiển thị loading, error, data
AsyncNotifier khi được watch cũng trả về AsyncValue, do đó bạn vẫn dùng .when() như với FutureProvider.
Best Practice: Đối với hầu hết các màn hình gọi API, hãy ưu tiên dùng FutureProvider hoặc AsyncNotifier. Không dùng StateProvider để lưu trữ loading/error/data thủ công.
6. Chuyển đổi từ Provider sang Riverpod (code so sánh)
Đây là phần dành riêng cho các bạn đã dùng Provider và muốn hình dung cách chuyển đổi.
6.1. MultiProvider → ProviderScope
Provider:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterModel()),
Provider(create: (_) => SomeService()),
],
child: MyApp(),
),
);
}
Riverpod:
void main() {
runApp(
const ProviderScope( // Chỉ một cấp duy nhất, không cần MultiProvider hay lồng nhau
child: MyApp(),
),
);
}
// Các provider được khai báo là top-level variable, không cần đặt trong widget tree.
final counterProvider = StateProvider
<int>((ref) => 0);
final someServiceProvider = Provider
<SomeService>((ref) => SomeService());
6.2. ChangeNotifierProvider → NotifierProvider
Provider (dùng ChangeNotifier):
class CartModel extends ChangeNotifier {
List
<String> items = [];
void add(String item) {
items.add(item);
notifyListeners(); // 👈 Phải gọi thủ công
}
}
final cartProvider = ChangeNotifierProvider((ref) => CartModel());
Riverpod (dùng NotifierProvider):
class CartNotifier extends Notifier<List<String>> {
@override
List
<String> build() => [];
void add(String item) {
state = [...state, item]; // 👈 Chỉ cần gán state =, tự động rebuild
}
}
final cartProvider = NotifierProvider<CartNotifier, List<String>>(CartNotifier.new);
6.3. ConsumerWidget thay vì Consumer
Provider:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = context.watch
<CartModel>();
return Text('${cart.items.length}');
}
}
Riverpod:
class MyWidget extends ConsumerWidget { // 👈 extends ConsumerWidget
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
return Text('${cart.length}');
}
}
7. Những lỗi thường gặp khi dùng Riverpod (Common Mistakes)
- Quên bọc
ProviderScope→ lỗiProviderNotFoundExceptionngay khi khởi động. - Dùng
ref.readtrong build method → widget không rebuild khi state thay đổi. Chỉ dùngref.watchtrong build. - Mutate state trực tiếp trong
Notifier(ví dụstate.add(todo)) → không trigger rebuild. Phải gánstate = ...với object mới. - Không dispose
TextEditingController→ memory leak (đã sửa trong ví dụ). - Dùng
StateProvidercho object phức tạp → khó kiểm soát và dễ bug. Hãy dùngNotifierProvider.
8. Tổng kết và lộ trình học tiếp
8.1. Khi nào dùng loại provider nào
| Loại Provider | Khi nào dùng? | Khi nào KHÔNG nên dùng? |
|---|---|---|
Provider |
Dependency injection, giá trị chỉ đọc không thay đổi (repository, service, config). | State cần thay đổi. |
StateProvider |
State đơn giản, 1-2 field (counter, checkbox, text). | Object có nhiều field hoặc logic phức tạp. |
FutureProvider |
Gọi API một lần khi vào màn hình, không cần tương tác thêm. | Cần refresh, submit, delete. |
NotifierProvider |
Hầu hết state trong app thực: có nhiều field, có logic thay đổi phức tạp. | State chỉ đọc đơn giản. |
AsyncNotifierProvider |
Gọi API kèm theo các method refresh, submit, delete – kết hợp cả async và business logic. | API chỉ gọi một lần, không cần tương tác. |
StreamProvider |
Lắng nghe realtime data (Firestore, WebSocket). | Dữ liệu tĩnh. |
8.2. Các khái niệm nâng cao: family, auto-dispose, ref.invalidate
Sau khi nắm vững các kiến thức trên, bạn có thể tìm hiểu thêm:
.family: Khi cần tạo nhiều provider instance với cùng một cấu trúc nhưng khác tham số đầu vào (ví dụ:postProvider('id1'),postProvider('id2'))..autoDispose: Tự động huỷ state của provider khi không còn widget nào theo dõi nữa, tránh rò rỉ bộ nhớ. Trong code generation với@riverpod, mặc định đã làautoDispose.ref.invalidate: Buộc Riverpod đánh dấu provider là “cũ” và tái tạo lại nó. Rất hữu ích khi muốn refresh dữ liệu từ API sau một hành động (thay vì dùng StateProvider để reload).
8.3. Thực hành ngay
9. FAQ (Câu hỏi thường gặp)
1. Riverpod có thay thế hoàn toàn được Provider không?
Riverpod được thiết kế để thay thế Provider trong hầu hết các trường hợp. Nó là phiên bản nâng cấp, an toàn và linh hoạt hơn. Tuy nhiên, các dự án nhỏ và cũ vẫn có thể dùng Provider. Nếu bắt đầu dự án mới hoặc có thời gian, bạn nên chuyển sang Riverpod.
2. Có nên dùng Riverpod cho project nhỏ?
Có. Riverpod rất nhẹ và có thể dùng cho mọi quy mô. Với project nhỏ bạn có thể chỉ dùng StateProvider và FutureProvider. Không có over-engineering.
3. Làm sao để test Riverpod provider?
Riverpod rất dễ test vì provider không phụ thuộc vào widget tree. Bạn tạo ProviderContainer riêng, override provider nếu cần, và kiểm tra state bằng cách gọi method. Mỗi test có thể có container riêng, không ảnh hưởng lẫn nhau.
test('counter increments', () {
final container = ProviderContainer();
final counter = container.read(counterProvider.notifier);
counter.update((value) => value + 1);
expect(container.read(counterProvider), 1);
});
4. ref.invalidate dùng khi nào?
Dùng khi bạn muốn buộc provider refresh dữ liệu, ví dụ sau khi thêm mới, xoá hoặc khi user kéo thả refresh. Gọi ref.invalidate(myProvider) sẽ làm cho myProvider được tạo lại từ đầu khi có người watch nó lần tiếp theo.
5. Riverpod có hoạt động với Flutter Web không?
Có, Riverpod hoạt động hoàn hảo trên Flutter Web, iOS, Android, Windows, macOS và Linux.
6. Tôi có cần dùng code generator (riverpod_generator) không?
Không bắt buộc. Code generator giúp viết ngắn gọn hơn và tránh lỗi chính tả, nhưng với người mới, viết thủ công (cách trong bài) dễ hiểu hơn. Khi đã quen, hãy chuyển sang generator.