Riverpod Flutter: Hướng Dẫn Toàn Diện Từ Cơ Bản Đến Nâng Cao (Cập Nhật 2026)

HNQ

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 độ → ProviderNotFoundException khi 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 String khá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 ProviderNotFoundException khi 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 userNameProviderfinal apiUrlProvider – hoàn toàn độc lập.
  • Hỗ trợ async tuyệt vời: FutureProviderAsyncNotifier xử lý loading/error/data mặc định, không cần dùng FutureBuilder thủ 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ùng ref.read trong 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)

  1. Quên bọc ProviderScope → lỗi ProviderNotFoundException ngay khi khởi động.
  2. Dùng ref.read trong build method → widget không rebuild khi state thay đổi. Chỉ dùng ref.watch trong build.
  3. Mutate state trực tiếp trong Notifier (ví dụ state.add(todo)) → không trigger rebuild. Phải gán state = ... với object mới.
  4. Không dispose TextEditingController → memory leak (đã sửa trong ví dụ).
  5. Dùng StateProvider cho object phức tạp → khó kiểm soát và dễ bug. Hãy dùng NotifierProvider.

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 StateProviderFutureProvider. 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.

Chia sẻ bài viết này
Hãy để lại bình luận