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

Language Switching trong Flutter

        Trong quá trình phát triển ứng dụng di động, khả năng hỗ trợ nhiều ngôn ngữ ngày càng trở nên quan trọng. Một ứng dụng chỉ sử dụng duy nhất một ngôn ngữ có thể gặp khó khăn khi tiếp cận người dùng ở các quốc gia và khu vực khác […]

UTC & Local Time Conversion

Trong thời đại toàn cầu hóa, con người thường xuyên làm việc, học tập và giao tiếp với những người ở các quốc gia khác nhau. Một cuộc họp trực tuyến có thể được tổ chức tại Việt Nam, nhưng người tham gia lại đang ở Hoa Kỳ, Anh, Nhật Bản hoặc Australia. Vì vậy, […]

Logging / Monitoring / CI/CD

1. Logging / Monitoring / CI/CD là gì? Trong quá trình phát triển phần mềm, việc viết code để ứng dụng chạy được chỉ là một phần của công việc. Khi đưa ứng dụng vào sử dụng thực tế, lập trình viên còn phải quan tâm đến nhiều vấn đề khác như ứng dụng có […]

Docker

1. Docker là gì? Docker là một nền tảng được sử dụng để đóng gói, triển khai và chạy ứng dụng trong các môi trường độc lập, được gọi là Container. Docker giúp lập trình viên đưa mã nguồn, thư viện, các gói phụ thuộc và những thành phần cần thiết để chạy ứng dụng […]

Unit Testing và Integration Testing

1. Unit Testing và Integration Testing là gì? Unit Testing và Integration Testing là hai phương pháp kiểm thử phần mềm thường được sử dụng trong quá trình phát triển ứng dụng. Mục đích của hai phương pháp này là kiểm tra xem chương trình có hoạt động đúng theo yêu cầu hay không, đồng […]

Message Broker

1. Message Broker là gì? Message Broker là một thành phần trung gian được sử dụng để tiếp nhận, quản lý và chuyển tiếp thông điệp giữa các ứng dụng hoặc các thành phần trong một hệ thống phần mềm. Công nghệ này thường xuất hiện trong những hệ thống Backend, kiến trúc Microservices và […]

Leave a Reply

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