Riverpod là một thư viện quản lý trạng thái phổ biến trong hệ sinh thái Flutter. Thư viện này cung cấp nhiều khái niệm, trong đó Provider và Notifier giữ vai trò đặc biệt quan trọng. Provider chịu trách nhiệm cung cấp dữ liệu hoặc một đối tượng cho các thành phần khác trong ứng dụng, còn Notifier đại diện cho một đối tượng có khả năng quản lý và thay đổi trạng thái theo một cách có tổ chức. Sự kết hợp giữa hai khái niệm này giúp ứng dụng tách biệt rõ ràng giữa giao diện, dữ liệu và logic nghiệp vụ.

1. Provider là gì?

Trong Riverpod, Provider có thể được hiểu là một đối tượng chịu trách nhiệm cung cấp dữ liệu hoặc một giá trị cho các thành phần khác trong ứng dụng. Provider không nhất thiết phải chứa trạng thái có thể thay đổi. Nó thường được dùng để cung cấp các giá trị cố định, tạo đối tượng dùng chung hoặc tính toán dữ liệu dựa trên những provider khác.

Một ví dụ đơn giản:

final appNameProvider = Provider<String>((ref) {
  return 'Ứng dụng học Flutter';
});

Trong ví dụ này, appNameProvider cung cấp một chuỗi. Widget có thể đọc giá trị bằng ref.watch:

class HomePage extends ConsumerWidget {
  const HomePage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final appName = ref.watch(appNameProvider);
    return Scaffold(
      appBar: AppBar(title: Text(appName)),
      body: const Center(child: Text('Trang chủ')),
    );
  }
}

Provider đóng vai trò như một nguồn dữ liệu trung gian. Widget không cần biết dữ liệu được tạo ra ở đâu hoặc logic bên trong provider hoạt động như thế nào. Điều này giúp giảm sự phụ thuộc giữa giao diện và logic ứng dụng.

2. Các loại Provider phổ biến

Riverpod cung cấp nhiều loại provider phục vụ những nhu cầu khác nhau.

Provider cơ bản

Provider phù hợp với dữ liệu chỉ đọc hoặc các đối tượng không cần thay đổi thường xuyên. Nó có thể cung cấp một hằng số, một service, một repository hoặc một giá trị được tính toán.

final apiClientProvider = Provider<ApiClient>((ref) {
  return ApiClient();
});

FutureProvider

FutureProvider dùng cho các tác vụ bất đồng bộ, chẳng hạn như gọi API hoặc đọc dữ liệu từ cơ sở dữ liệu.

final usersProvider = FutureProvider<List<User>>((ref) async {
  final apiClient = ref.watch(apiClientProvider);
  return apiClient.fetchUsers();
});

Khi sử dụng provider này, giao diện có thể xử lý ba trạng thái phổ biến: đang tải, thành công và lỗi.

StreamProvider

StreamProvider phù hợp với dữ liệu được cập nhật liên tục, chẳng hạn như tin nhắn thời gian thực, dữ liệu cảm biến hoặc trạng thái kết nối.

StateProvider

StateProvider có thể được dùng cho trạng thái đơn giản như một số nguyên, chuỗi hoặc giá trị boolean. Tuy nhiên, đối với logic phức tạp, nên sử dụng Notifier để mã nguồn rõ ràng và dễ mở rộng hơn.

3. Notifier là gì?

Notifier là một lớp chuyên dùng để quản lý trạng thái có thể thay đổi. Khác với Provider cơ bản chỉ cung cấp dữ liệu, Notifier thường chứa cả trạng thái hiện tại và các phương thức thay đổi trạng thái đó.

Ví dụ, một bộ đếm có thể được xây dựng như sau:

class CounterNotifier extends Notifier<int> {
  @override
  int build() {
    return 0;
  }
  void increment() {
    state++;
  }
  void decrement() {
    state--;
  }
}
final counterProvider = NotifierProvider<CounterNotifier, int>(
  CounterNotifier.new,
);

Widget sử dụng Notifier như sau:

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).increment();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Trong đoạn mã trên, ref.watch(counterProvider) dùng để lắng nghe giá trị trạng thái. Khi phương thức increment được gọi, trạng thái thay đổi và widget tự động cập nhật. Trong khi đó, ref.read(counterProvider.notifier) được dùng để lấy đối tượng Notifier nhằm gọi các phương thức xử lý hành động.

4. Mối quan hệ giữa Provider và Notifier

Provider và Notifier không phải là hai khái niệm hoàn toàn tách biệt. Trong Riverpod, Notifier thường được đăng ký và sử dụng thông qua một loại provider, chẳng hạn như NotifierProvider hoặc AsyncNotifierProvider.

Có thể hình dung mối quan hệ này như sau:

  • Notifier chứa logic và quản lý trạng thái.
  • NotifierProvider làm cầu nối để cung cấp trạng thái của Notifier cho giao diện.
  • Widget đọc trạng thái từ provider và gửi hành động đến Notifier.

Mô hình này tạo ra sự phân tách trách nhiệm rõ ràng. Giao diện chịu trách nhiệm hiển thị, Notifier chịu trách nhiệm xử lý logic và provider chịu trách nhiệm kết nối các phần với nhau.

5. AsyncNotifier và trạng thái bất đồng bộ

Trong các ứng dụng thực tế, dữ liệu thường được tải từ máy chủ hoặc cơ sở dữ liệu. Vì vậy, trạng thái không chỉ có một giá trị đơn giản mà còn bao gồm trạng thái đang tải và trạng thái lỗi. AsyncNotifier được thiết kế cho trường hợp này.

class UserNotifier extends AsyncNotifier<User?> {
  @override
  Future<User?> build() async {
    return null;
  }
  Future<void> login(String email, String password) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final repository = ref.read(userRepositoryProvider);
      return repository.login(email, password);
    });
  }
}
final userProvider = AsyncNotifierProvider<UserNotifier, User?>(
  UserNotifier.new,
);

AsyncNotifier giúp tập trung logic bất đồng bộ vào một nơi. Thay vì tự tạo nhiều biến như isLoading, errorMessage và user, lập trình viên có thể sử dụng AsyncValue để biểu diễn các trạng thái tải dữ liệu một cách nhất quán.

Các trạng thái thường gặp gồm:

  • AsyncLoading: dữ liệu đang được tải.
  • AsyncData: dữ liệu đã tải thành công.
  • AsyncError: quá trình tải dữ liệu xảy ra lỗi.

Cách tiếp cận này làm cho giao diện dễ đọc hơn và giảm nguy cơ bỏ sót trường hợp xử lý lỗi.

6. So sánh Provider và Notifier

Tiêu chí Provider Notifier
Mục đích chính Cung cấp dữ liệu hoặc đối tượng Quản lý trạng thái có thể thay đổi
Chứa phương thức thay đổi trạng thái Thường không Có
Phù hợp với dữ liệu chỉ đọc Rất phù hợp Có thể, nhưng thường không cần thiết
Phù hợp với logic nghiệp vụ Hạn chế Phù hợp
Khả năng mở rộng Tốt với logic đơn giản Tốt với logic phức tạp
Tổ chức mã nguồn Ngắn gọn Có cấu trúc rõ ràng hơn
Xử lý trạng thái bất đồng bộ Dùng FutureProvider hoặc StreamProvider Dùng AsyncNotifier

 

Từ bảng trên có thể thấy Provider phù hợp với vai trò cung cấp dữ liệu, còn Notifier phù hợp với việc điều khiển và thay đổi dữ liệu. Trong ứng dụng thực tế, hai thành phần này thường được sử dụng kết hợp thay vì lựa chọn một trong hai.

7. Khi nào nên sử dụng Provider/Notifier?

Provider nên được sử dụng trong những trường hợp sau:

  • Cung cấp dependency: Ví dụ API client, repository, database service hoặc một cấu hình ứng dụng.
  • Cung cấp dữ liệu chỉ đọc: Ví dụ tên ứng dụng, thông tin phiên bản hoặc các hằng số.
  • Tính toán dữ liệu từ provider khác: Provider có thể đọc các nguồn dữ liệu khác và tạo ra kết quả mới.
  • Tách logic khởi tạo đối tượng khỏi widget: Widget chỉ cần đọc đối tượng mà không cần tự tạo hoặc quản lý vòng đời của nó.

Ví dụ, repository có thể được cung cấp bằng Provider và được sử dụng bên trong Notifier. Cách tổ chức này giúp Notifier không phụ thuộc trực tiếp vào cách khởi tạo repository.

Notifier nên được sử dụng khi trạng thái có nhiều hành động hoặc có logic nghiệp vụ đáng kể. Một số trường hợp phổ biến gồm:

  • Quản lý giỏ hàng.
  • Quản lý trạng thái đăng nhập.
  • Quản lý biểu mẫu.
  • Quản lý bộ lọc và sắp xếp danh sách.
  • Quản lý trạng thái tải, thành công và lỗi.
  • Thực hiện các thao tác thêm, sửa, xóa dữ liệu.

Ví dụ, thay vì để widget trực tiếp thay đổi danh sách sản phẩm, lập trình viên có thể định nghĩa các phương thức như addProduct, removeProduct và clearCart trong CartNotifier. Nhờ đó, các quy tắc nghiệp vụ được tập trung và có thể tái sử dụng ở nhiều màn hình.

8. Ưu điểm chung của Provider và Notifier

Tách biệt giao diện và logic

Provider và Notifier giúp đưa logic ra khỏi widget. Widget trở nên ngắn gọn, tập trung vào việc hiển thị dữ liệu. Đây là yếu tố quan trọng trong các dự án lớn, nơi giao diện và nghiệp vụ thường thay đổi độc lập.

Khả năng tái sử dụng

Một provider có thể được nhiều widget sử dụng. Notifier cũng có thể cung cấp cùng một logic cho nhiều màn hình khác nhau mà không cần sao chép mã nguồn.

Dễ kiểm thử

Vì logic được đặt trong provider hoặc Notifier thay vì nằm rải rác trong widget, lập trình viên có thể kiểm thử độc lập các phương thức xử lý trạng thái. Điều này làm tăng độ tin cậy của ứng dụng.

Quản lý dependency hiệu quả

Provider hỗ trợ việc cung cấp các dependency theo một cấu trúc rõ ràng. Khi cần thay đổi implementation, chẳng hạn chuyển từ API thật sang API giả để kiểm thử, lập trình viên chỉ cần thay thế provider tương ứng.

Hỗ trợ trạng thái bất đồng bộ

Thông qua FutureProvider, StreamProvider và AsyncNotifier, Riverpod cung cấp cách xử lý thống nhất cho các thao tác bất đồng bộ. Trạng thái loading, data và error được biểu diễn rõ ràng, giảm lỗi trong quá trình phát triển.

Kết luận

Provider và Notifier đại diện cho hai vai trò bổ trợ trong kiến trúc quản lý trạng thái của Riverpod. Provider tạo ra một cơ chế linh hoạt để cung cấp dữ liệu, dependency và các giá trị được tính toán. Notifier mở rộng cơ chế đó bằng cách đóng gói trạng thái cùng các hành động làm thay đổi trạng thái.

Provider và Notifier là hai thành phần quan trọng trong Riverpod, góp phần giải quyết bài toán quản lý trạng thái trong ứng dụng Flutter. Provider phù hợp với việc cung cấp dữ liệu, dependency hoặc các giá trị chỉ đọc. Notifier phù hợp với việc quản lý trạng thái có thể thay đổi và chứa các phương thức xử lý logic nghiệp vụ.

Bài viết khác

REST API & HTTP trong Flutter

REST API & HTTP trong Flutter 1. REST API và HTTP 1.1. Khái niệm REST API REST API là một phương thức thiết kế giao tiếp giữa các hệ thống phần mềm thông qua giao thức HTTP. REST là viết tắt của Representational State Transfer, còn API là viết tắt của Application Programming Interface. REST […]

Dio & HTTP Client trong Flutter

Dio & HTTP Client trong Flutter 1. Tổng quan về Dio và HTTP Client Trong quá trình phát triển ứng dụng Flutter, việc giao tiếp với máy chủ (Server) là một phần quan trọng để ứng dụng có thể lấy dữ liệu, gửi thông tin, đăng nhập, đăng ký tài khoản, tải hình ảnh, cập […]

Flutter Widget Fundamentals – Tối ưu hiệu năng và kiểm thử Widget

Tối ưu hiệu năng và kiểm thử Widget trong Flutter 1. Rebuild Optimization – Tối ưu quá trình xây dựng lại giao diện 1.1. Khái niệm Rebuild Optimization Rebuild Optimization là quá trình tối ưu việc xây dựng lại giao diện trong Flutter nhằm giảm những lần thực thi build() không cần thiết, hạn chế […]

Equivalence Partitioning(Phân vùng tương đương)

1.Equivalence Partitioning (Phân vùng tương đương) là gì? Equivalence Partitioning (Phân vùng tương đương) là một trong những kỹ thuật thiết kế kiểm thử hộp đen (Black-box Testing Techniques) cơ bản và quan trọng nhất trong kỹ nghệ phần mềm. Kỹ thuật này được áp dụng nhằm giảm thiểu số lượng ca kiểm thử (Test […]

Flutter Widget Fundamentals – Các cơ chế UI nâng cao trong Flutter

Các cơ chế UI nâng cao trong Flutter 1. Gesture System và Hit Testing 1.1. Tổng quan về Gesture System Gesture System là hệ thống xử lý cử chỉ tương tác trong Flutter, cho phép ứng dụng nhận biết và phản hồi những hành động của người dùng như chạm, nhấn giữ, kéo thả, vuốt, […]

Flutter Widget Fundamentals – Vòng đời và quản lý trạng thái của Widget

Vòng đời và quản lý trạng thái của Widget trong Flutter 1. StatefulWidget Lifecycle chuyên sâu 1.1. Khái niệm vòng đời của StatefulWidget Trong Flutter, vòng đời của StatefulWidget là chuỗi các giai đoạn mà một Widget trải qua từ khi được tạo, khởi tạo trạng thái, hiển thị giao diện, nhận các thay đổi […]

Leave a Reply

Your email address will not be published. Required fields are marked *