Provider & Notifier trong Flutter
1. Định nghĩa Provider và Notifier
Trong Flutter, Provider và Notifier là hai khái niệm thường được sử dụng trong quản lý trạng thái (State Management). Chúng giúp tổ chức dữ liệu, xử lý logic nghiệp vụ và cập nhật giao diện khi dữ liệu của ứng dụng thay đổi.
Khi xây dựng ứng dụng Flutter, dữ liệu có thể thay đổi liên tục, chẳng hạn như số lượng sản phẩm trong giỏ hàng, thông tin người dùng đăng nhập, danh sách công việc, trạng thái tải dữ liệu từ API hoặc cài đặt giao diện sáng và tối. Nếu quản lý tất cả dữ liệu trực tiếp bên trong từng Widget, mã nguồn sẽ dễ trở nên phức tạp, khó bảo trì và khó tái sử dụng.
Provider và Notifier giúp giải quyết vấn đề này bằng cách tách biệt dữ liệu và logic xử lý khỏi giao diện người dùng.
Provider là cơ chế cung cấp và phân phối dữ liệu hoặc đối tượng quản lý trạng thái đến các thành phần cần sử dụng. Trong hệ sinh thái Riverpod, Provider còn là một hệ thống khai báo các nguồn dữ liệu, cho phép truy cập, theo dõi và quản lý vòng đời của những nguồn dữ liệu đó.
Notifier là một đối tượng chịu trách nhiệm quản lý trạng thái và cung cấp các phương thức để thay đổi trạng thái. Khi trạng thái thay đổi, những Widget đang theo dõi trạng thái đó có thể được thông báo và cập nhật giao diện tương ứng.
Ví dụ, trong ứng dụng bán hàng, Provider có thể cung cấp đối tượng quản lý giỏ hàng cho nhiều màn hình. Notifier bên trong đối tượng đó chịu trách nhiệm thêm sản phẩm, xóa sản phẩm, thay đổi số lượng và tính tổng giá trị đơn hàng.
Hai khái niệm này thường được sử dụng kết hợp trong Riverpod, một thư viện quản lý trạng thái phổ biến của Flutter.
2. Tổng quan về cơ chế quản lý trạng thái
2.1. Khái niệm State Management
State Management là quá trình quản lý dữ liệu có thể thay đổi trong suốt vòng đời hoạt động của ứng dụng.
State, hay trạng thái, là tập hợp các giá trị quyết định giao diện và hành vi hiện tại của ứng dụng.
Ví dụ, một màn hình đăng nhập có thể có các trạng thái sau:
-
Chưa nhập thông tin tài khoản.
-
Đang nhập tên đăng nhập và mật khẩu.
-
Đang gửi yêu cầu đăng nhập đến máy chủ.
-
Đăng nhập thành công.
-
Đăng nhập thất bại và hiển thị thông báo lỗi.
Mỗi trạng thái có thể dẫn đến một giao diện khác nhau. Khi người dùng nhấn nút đăng nhập, ứng dụng chuyển từ trạng thái bình thường sang trạng thái đang xử lý. Khi máy chủ trả về kết quả, ứng dụng tiếp tục chuyển sang trạng thái thành công hoặc thất bại.
Nếu không có cơ chế quản lý trạng thái rõ ràng, lập trình viên phải tự tổ chức dữ liệu và gọi cập nhật giao diện ở nhiều vị trí. Điều này khiến việc đồng bộ dữ liệu giữa các màn hình trở nên khó khăn.
State Management cung cấp một phương pháp có tổ chức để lưu trữ trạng thái, thay đổi trạng thái và thông báo cho giao diện khi có dữ liệu mới.
2.2. Vai trò của Provider và Notifier trong State Management
Provider và Notifier đảm nhận những nhiệm vụ khác nhau nhưng bổ trợ cho nhau.
| Thành phần | Vai trò chính |
|---|---|
| Provider | Khai báo, cung cấp và cho phép truy cập dữ liệu hoặc đối tượng quản lý trạng thái |
| Notifier | Quản lý trạng thái và định nghĩa các phương thức thay đổi trạng thái |
| Widget | Hiển thị dữ liệu và tiếp nhận tương tác từ người dùng |
| Ref | Cung cấp các phương thức để truy cập, theo dõi hoặc tương tác với Provider trong Riverpod |
Có thể hình dung luồng hoạt động như sau:
Người dùng tương tác (Nhấn nút, nhập dữ liệu hoặc chọn sản phẩm) -> Notifier (Xử lý logic và cập nhật trạng thái) -> Provider / Riverpod (Quản lý và cung cấp trạng thái mới) -> Widget cập nhật giao diện (Hiển thị dữ liệu mới cho người dùng)
Sơ đồ trên mô tả luồng khái quát. Trong Riverpod, Provider khai báo nguồn trạng thái và liên kết với Notifier; Notifier phát ra trạng thái mới, còn các Widget đang theo dõi nguồn đó sẽ được cập nhật khi cần thiết.
3. Provider trong Flutter và Riverpod
3.1. Khái niệm Provider
Provider có thể được hiểu là một nguồn cung cấp dữ liệu hoặc một đối tượng mà các thành phần trong ứng dụng có thể truy cập khi cần.
Trong Flutter, thuật ngữ Provider thường được sử dụng theo hai nghĩa:
-
Provider package: Thư viện quản lý trạng thái sử dụng cơ chế như
ChangeNotifierProviderđể cung cấp đối tượng cho Widget. -
Riverpod: Hệ thống quản lý trạng thái có các loại Provider như
Provider,StateProvidertrong các phiên bản cũ,NotifierProvidervàFutureProvider.
Hai thư viện có những điểm tương đồng nhưng API và cách tổ chức khác nhau. Vì vậy, cần xác định rõ đang sử dụng Provider package hay Riverpod khi viết mã nguồn.
Trong nội dung này, các ví dụ chính sử dụng Riverpod.
Provider giúp giảm sự phụ thuộc trực tiếp giữa các Widget. Thay vì truyền dữ liệu qua nhiều lớp Widget, một Widget có thể đọc dữ liệu từ Provider đã được khai báo.
Ví dụ, ứng dụng có màn hình danh sách sản phẩm và màn hình chi tiết sản phẩm. Cả hai màn hình có thể sử dụng chung một nguồn dữ liệu hoặc một đối tượng quản lý trạng thái mà không cần truyền toàn bộ dữ liệu qua từng Widget trung gian.
3.2. Những nhiệm vụ chính của Provider
Cung cấp dữ liệu
Provider cho phép khai báo các nguồn dữ liệu như cấu hình ứng dụng, thông tin người dùng, danh sách sản phẩm hoặc kết quả truy vấn dữ liệu.
Tái sử dụng logic
Một Provider có thể được nhiều Widget sử dụng. Điều này hạn chế việc viết lại cùng một logic ở nhiều màn hình.
Theo dõi sự thay đổi
Khi Widget sử dụng cơ chế theo dõi như ref.watch, Riverpod có thể xây dựng mối quan hệ phụ thuộc giữa Widget và Provider. Khi giá trị được theo dõi thay đổi, Widget có thể được xây dựng lại.
Quản lý vòng đời
Riverpod hỗ trợ quản lý vòng đời của các Provider và trạng thái liên quan. Với những Provider được cấu hình tự động hủy, trạng thái có thể được giải phóng khi không còn được sử dụng và đáp ứng các điều kiện của cơ chế đó.
Tổ chức sự phụ thuộc
Một Provider có thể sử dụng dữ liệu từ Provider khác. Chẳng hạn, Provider lấy danh sách sản phẩm có thể phụ thuộc vào Provider chứa đối tượng API hoặc thông tin xác thực.
3.3. Các loại Provider thường gặp trong Riverpod
| Loại Provider | Chức năng |
|---|---|
Provider |
Cung cấp một giá trị hoặc đối tượng không trực tiếp quản lý trạng thái có thể thay đổi theo cách thông thường |
NotifierProvider |
Cung cấp Notifier đồng bộ cùng trạng thái do Notifier quản lý |
AsyncNotifierProvider |
Cung cấp Notifier phù hợp với trạng thái bất đồng bộ, chẳng hạn dữ liệu tải từ API |
FutureProvider |
Cung cấp kết quả của một tác vụ bất đồng bộ trả về Future |
StreamProvider |
Theo dõi dữ liệu phát ra liên tục từ Stream |
ChangeNotifierProvider |
Tích hợp đối tượng ChangeNotifier, thường dùng khi làm việc với mô hình Provider truyền thống hoặc mã nguồn tương thích |
Lưu ý: StateProvider từng được sử dụng rộng rãi để quản lý những trạng thái đơn giản trong Riverpod, nhưng ở các phiên bản Riverpod mới, API này đã được đánh dấu không còn khuyến nghị trong nhiều trường hợp. Khi bắt đầu dự án mới, nên ưu tiên những API phù hợp với phiên bản Riverpod đang sử dụng, chẳng hạn NotifierProvider.
3.4. Ví dụ sử dụng Provider
Ví dụ dưới đây tạo một Provider cung cấp tên ứng dụng.
import 'package:flutter_riverpod/flutter_riverpod.dart';
final appNameProvider = Provider<String>((ref) {
return 'Smart Commerce';
});
Trong đoạn mã trên:
-
appNameProviderlà tên của Provider. -
Provider<String>xác định giá trị được cung cấp có kiểu dữ liệuString. -
Hàm callback nhận
ref, cho phép truy cập các Provider khác nếu cần. -
Giá trị trả về là tên ứng dụng.
Để sử dụng Provider trong giao diện, ứng dụng cần có ProviderScope ở cấp phù hợp, thường được đặt tại điểm khởi chạy ứng dụng.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appName = ref.watch(appNameProvider);
return MaterialApp(
title: appName,
home: Scaffold(
appBar: AppBar(
title: Text(appName),
),
body: Center(
child: Text('Chào mừng đến với $appName'),
),
),
);
}
}
Trong ví dụ này, ProviderScope thiết lập phạm vi để Riverpod quản lý Provider. ConsumerWidget cho phép Widget truy cập WidgetRef, còn ref.watch đọc và theo dõi giá trị từ appNameProvider.
Đây là ví dụ về Provider cung cấp một giá trị. Giá trị này không tự thay đổi khi người dùng tương tác, nhưng nó có thể làm cơ sở để những Provider khác xây dựng dữ liệu phụ thuộc.
4. Notifier trong Riverpod
4.1. Khái niệm Notifier
Notifier là lớp quản lý trạng thái và định nghĩa những hành động có thể làm thay đổi trạng thái đó.
Thay vì để giao diện trực tiếp xử lý toàn bộ logic, lập trình viên đặt các thao tác như thêm sản phẩm, xóa công việc, cập nhật thông tin cá nhân hoặc tải lại dữ liệu vào Notifier.
Widget chỉ cần gọi phương thức tương ứng. Notifier sẽ xử lý logic, cập nhật trạng thái và thông báo cho hệ thống quản lý trạng thái.
Trong Riverpod hiện đại, Notifier và AsyncNotifier là hai lớp quan trọng.
-
Notifier<T>phù hợp với trạng thái đồng bộ, trong đóTlà kiểu dữ liệu của trạng thái. -
AsyncNotifier<T>phù hợp với trạng thái có quá trình bất đồng bộ, chẳng hạn tải danh sách sản phẩm từ máy chủ.
Notifier không phải là một Provider độc lập theo nghĩa cung cấp dữ liệu cho Widget. Thông thường, Notifier được liên kết với NotifierProvider hoặc AsyncNotifierProvider, thông qua đó trạng thái được truy cập và theo dõi.
4.2. Nhiệm vụ của Notifier
Lưu trữ và quản lý trạng thái
Notifier cung cấp giá trị trạng thái hiện tại thông qua thuộc tính state. Kiểu dữ liệu của state phụ thuộc vào cách khai báo Notifier.
Ví dụ, nếu trạng thái là số nguyên, state có thể lưu số lượng sản phẩm. Nếu trạng thái là danh sách, state có thể lưu danh sách công việc.
Xử lý logic nghiệp vụ
Notifier là nơi thích hợp để tổ chức các quy tắc của ứng dụng, chẳng hạn không cho phép số lượng sản phẩm nhỏ hơn 0 hoặc kiểm tra dữ liệu trước khi lưu.
Cung cấp phương thức thao tác
Các phương thức như increment(), addProduct() hoặc removeTask() cho phép Widget yêu cầu thay đổi trạng thái mà không cần biết chi tiết cách xử lý bên trong.
Thông báo trạng thái mới
Khi Notifier gán một giá trị mới cho state, Riverpod sẽ xử lý việc thông báo đến các thành phần đang theo dõi trạng thái đó theo cơ chế của Provider tương ứng.
Tách biệt giao diện và logic
Notifier không cần trực tiếp xây dựng Widget. Nhờ đó, logic có thể được kiểm thử độc lập với giao diện, đồng thời dễ tái sử dụng trên nhiều màn hình.
4.3. Cấu trúc cơ bản của Notifier
Một Notifier đồng bộ thường có cấu trúc như sau:
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterNotifier extends Notifier<int> {
@override
int build() {
return 0;
}
void increment() {
state++;
}
void decrement() {
if (state > 0) {
state--;
}
}
void reset() {
state = 0;
}
}
final counterProvider =
NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);
Trong ví dụ trên, CounterNotifier quản lý trạng thái kiểu int. Phương thức build() khởi tạo trạng thái ban đầu bằng 0.
Các phương thức increment(), decrement() và reset() chịu trách nhiệm thay đổi trạng thái. counterProvider kết nối lớp Notifier với hệ thống Provider của Riverpod.
Khi một Widget theo dõi counterProvider, Widget có thể nhận được giá trị mới sau mỗi lần trạng thái thay đổi.
4.4. Phương thức build trong Notifier
build() là phương thức quan trọng dùng để khởi tạo trạng thái của Notifier.
Đối với Notifier đồng bộ, build() thường trả về một giá trị hoặc cấu trúc dữ liệu ban đầu. Đối với AsyncNotifier, build() có thể trả về Future<T> để khởi tạo trạng thái bằng dữ liệu bất đồng bộ.
Ví dụ:
class CounterNotifier extends Notifier<int> {
@override
int build() {
return 10;
}
}
Trong trường hợp này, trạng thái ban đầu là 10.
Phương thức build() cũng có thể đọc các Provider phụ thuộc thông qua ref. Khi những phụ thuộc được theo dõi thay đổi, Riverpod có thể chạy lại quá trình xây dựng trạng thái theo cơ chế của Notifier.
Cần phân biệt build() với các phương thức xử lý hành động. build() chịu trách nhiệm thiết lập trạng thái ban đầu hoặc tái thiết lập trạng thái khi các phụ thuộc liên quan thay đổi; những phương thức như addProduct() hay removeProduct() được gọi khi cần thực hiện nghiệp vụ.
5. Sự kết hợp giữa Provider và Notifier
Provider và Notifier thường phát huy hiệu quả tốt nhất khi được sử dụng cùng nhau.
Provider tạo ra điểm truy cập cho trạng thái, trong khi Notifier định nghĩa cách trạng thái được khởi tạo và thay đổi.
Ví dụ, trong một ứng dụng bán hàng:
-
cartProvidercung cấp trạng thái giỏ hàng. -
CartNotifierquản lý danh sách sản phẩm trong giỏ hàng. -
Widget danh sách sản phẩm gọi phương thức thêm sản phẩm.
-
Widget giỏ hàng theo dõi trạng thái để hiển thị danh sách và tổng tiền.
Cấu trúc này cho phép nhiều Widget sử dụng cùng một nguồn trạng thái thay vì mỗi Widget tự lưu một bản sao riêng.
5.1. Ví dụ quản lý giỏ hàng
Đầu tiên, khai báo lớp dữ liệu đại diện cho sản phẩm.
class Product {
final String id;
final String name;
final double price;
final int quantity;
const Product({
required this.id,
required this.name,
required this.price,
this.quantity = 1,
});
Product copyWith({
String? id,
String? name,
double? price,
int? quantity,
}) {
return Product(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
quantity: quantity ?? this.quantity,
);
}
}
Lớp Product chứa mã sản phẩm, tên, giá bán và số lượng. Phương thức copyWith() tạo ra một đối tượng mới dựa trên đối tượng hiện tại, đồng thời cho phép thay đổi một số thuộc tính.
Tiếp theo, xây dựng Notifier để quản lý giỏ hàng.
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CartNotifier extends Notifier<List<Product>> {
@override
List<Product> build() {
return [];
}
void addProduct(Product product) {
final index = state.indexWhere(
(item) => item.id == product.id,
);
if (index == -1) {
state = [...state, product];
} else {
final updatedCart = [...state];
updatedCart[index] = updatedCart[index].copyWith(
quantity: updatedCart[index].quantity + 1,
);
state = updatedCart;
}
}
void removeProduct(String productId) {
state = state
.where((product) => product.id != productId)
.toList();
}
void increaseQuantity(String productId) {
state = [
for (final product in state)
if (product.id == productId)
product.copyWith(
quantity: product.quantity + 1,
)
else
product,
];
}
void decreaseQuantity(String productId) {
state = [
for (final product in state)
if (product.id == productId &&
product.quantity > 1)
product.copyWith(
quantity: product.quantity - 1,
)
else
product,
];
}
double get totalPrice {
return state.fold(
0,
(total, product) =>
total + product.price * product.quantity,
);
}
int get totalQuantity {
return state.fold(
0,
(total, product) => total + product.quantity,
);
}
void clearCart() {
state = [];
}
}
final cartProvider =
NotifierProvider<CartNotifier, List<Product>>(
CartNotifier.new,
);
Đoạn mã trên thể hiện những nhiệm vụ chính của Notifier trong một ứng dụng thực tế.
-
build()khởi tạo giỏ hàng rỗng. -
addProduct()thêm sản phẩm mới hoặc tăng số lượng nếu sản phẩm đã tồn tại. -
removeProduct()xóa sản phẩm theo mã. -
increaseQuantity()vàdecreaseQuantity()điều chỉnh số lượng. -
totalPricetính tổng tiền. -
totalQuantitytính tổng số lượng sản phẩm. -
clearCart()xóa toàn bộ giỏ hàng.
Các thao tác thay đổi danh sách đều tạo ra một danh sách mới trước khi gán vào state. Cách làm này giúp trạng thái được cập nhật rõ ràng và tránh sửa trực tiếp cấu trúc danh sách hiện có.
5.2. Hiển thị dữ liệu giỏ hàng trên giao diện
Sau khi tạo Notifier và Provider, Widget có thể sử dụng trạng thái để hiển thị thông tin.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CartPage extends ConsumerWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
final totalPrice =
ref.watch(cartProvider.notifier).totalPrice;
return Scaffold(
appBar: AppBar(
title: const Text('Giỏ hàng'),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.length,
itemBuilder: (context, index) {
final product = cart[index];
return ListTile(
title: Text(product.name),
subtitle: Text(
'${product.price} VNĐ',
),
trailing: Text(
'SL: ${product.quantity}',
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Tổng tiền: $totalPrice VNĐ',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
}
Ở đây, ref.watch(cartProvider) theo dõi danh sách sản phẩm. Khi danh sách thay đổi, Widget có thể được xây dựng lại để hiển thị dữ liệu mới.
Đối với totalPrice, ví dụ sử dụng ref.watch(cartProvider.notifier) để lấy đối tượng Notifier. Cách đọc này không tự theo dõi những thay đổi của thuộc tính totalPrice. Vì vậy, dù danh sách được cập nhật, việc đọc thuộc tính thông qua Notifier như trên không phải cách tối ưu để bảo đảm tổng tiền luôn được cập nhật đúng.
Một cách tổ chức tốt hơn là tạo Provider dẫn xuất từ trạng thái giỏ hàng.
final cartTotalProvider = Provider<double>((ref) {
final cart = ref.watch(cartProvider);
return cart.fold(
0,
(total, product) =>
total + product.price * product.quantity,
);
});
Sau đó, trong Widget:
final totalPrice = ref.watch(cartTotalProvider);
Khi giỏ hàng thay đổi, cartTotalProvider sẽ tính lại tổng tiền và Widget theo dõi tổng tiền sẽ được cập nhật. Cách này thể hiện rõ nguyên tắc xây dựng dữ liệu dẫn xuất từ trạng thái gốc.
5.3. Gọi phương thức của Notifier từ Widget
Để thực hiện hành động thay đổi trạng thái, Widget có thể gọi phương thức của Notifier thông qua ref.read.
Ví dụ, nút thêm sản phẩm:
ElevatedButton(
onPressed: () {
ref.read(cartProvider.notifier).addProduct(
const Product(
id: 'P001',
name: 'Tai nghe Bluetooth',
price: 500000,
),
);
},
child: const Text('Thêm vào giỏ hàng'),
)
Trong đoạn mã này:
-
ref.read()lấy đối tượng Notifier để thực hiện hành động. -
.notifiertruy cập đối tượng quản lý trạng thái. -
addProduct()thực hiện logic thêm sản phẩm. -
Notifier cập nhật
state. -
Các Widget đang theo dõi trạng thái liên quan nhận được dữ liệu mới.
Thông thường, ref.read phù hợp với các hành động như nhấn nút hoặc gửi biểu mẫu, còn ref.watch phù hợp khi giao diện cần phản ứng với sự thay đổi của dữ liệu.
6. AsyncNotifier và quản lý dữ liệu bất đồng bộ
Trong ứng dụng thực tế, dữ liệu không phải lúc nào cũng có sẵn ngay lập tức. Nhiều thao tác phải gửi yêu cầu đến API, truy vấn cơ sở dữ liệu hoặc đọc dữ liệu từ một dịch vụ bên ngoài.
Ví dụ, khi mở màn hình danh sách sản phẩm, ứng dụng cần gửi yêu cầu đến máy chủ và chờ kết quả. Trong thời gian đó, giao diện cần thể hiện trạng thái đang tải, thông báo lỗi nếu yêu cầu thất bại hoặc hiển thị dữ liệu khi thành công.
AsyncNotifier được thiết kế để quản lý những trạng thái có quá trình bất đồng bộ như vậy.
6.1. Các trạng thái bất đồng bộ
Một tác vụ tải dữ liệu thường có ba trạng thái chính:
-
Loading: Đang thực hiện tác vụ và chưa có kết quả cuối cùng.
-
Data: Tác vụ thành công và có dữ liệu trả về.
-
Error: Tác vụ thất bại và có thông tin lỗi.
Trong Riverpod, những trạng thái này thường được biểu diễn thông qua AsyncValue<T>. Kiểu dữ liệu này giúp giao diện xử lý kết quả bất đồng bộ theo cách thống nhất.
6.2. Ví dụ tải danh sách sản phẩm
Đầu tiên, tạo một lớp dịch vụ giả lập quá trình gọi API.
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ProductRepository {
Future<List<String>> fetchProducts() async {
await Future.delayed(
const Duration(seconds: 2),
);
return [
'Điện thoại',
'Laptop',
'Tai nghe',
];
}
}
final productRepositoryProvider =
Provider<ProductRepository>((ref) {
return ProductRepository();
});
Tiếp theo, tạo AsyncNotifier để quản lý danh sách sản phẩm.
class ProductNotifier
extends AsyncNotifier<List<String>> {
@override
Future<List<String>> build() {
return ref
.read(productRepositoryProvider)
.fetchProducts();
}
Future<void> reloadProducts() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref
.read(productRepositoryProvider)
.fetchProducts(),
);
}
}
final productProvider =
AsyncNotifierProvider<
ProductNotifier,
List<String>>(
ProductNotifier.new,
);
Trong ví dụ này, build() tải danh sách sản phẩm từ ProductRepository. Khi khởi tạo, trạng thái sẽ phản ánh tiến trình bất đồng bộ.
Phương thức reloadProducts() cho phép tải lại dữ liệu. AsyncValue.guard() hỗ trợ chuyển kết quả của tác vụ thành trạng thái dữ liệu hoặc lỗi, giúp hạn chế việc phải tự viết nhiều khối try-catch chỉ để biểu diễn kết quả bất đồng bộ.
Đối với ứng dụng thực tế, Repository có thể được triển khai bằng http, Dio, cơ sở dữ liệu hoặc một nguồn dữ liệu khác. Ví dụ trên chỉ mô phỏng cơ chế quản lý trạng thái, chưa thực hiện yêu cầu HTTP thật.
6.3. Hiển thị trạng thái bất đồng bộ
Widget có thể sử dụng when() để xử lý ba trường hợp.
class ProductListPage extends ConsumerWidget {
const ProductListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Danh sách sản phẩm'),
actions: [
IconButton(
onPressed: () {
ref
.read(productProvider.notifier)
.reloadProducts();
},
icon: const Icon(Icons.refresh),
),
],
),
body: products.when(
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text('Lỗi tải dữ liệu: $error'),
),
data: (items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
},
),
),
);
}
}
Cách tổ chức này giúp giao diện có thể phản ánh chính xác tình trạng tải dữ liệu. Logic gọi dịch vụ được đặt trong Notifier, còn Widget tập trung vào việc trình bày trạng thái.
Trong ứng dụng thương mại điện tử, mô hình này có thể được áp dụng cho danh sách sản phẩm, lịch sử đơn hàng, thông tin tài khoản, dữ liệu tồn kho và những nội dung được tải từ máy chủ.
7. Phân biệt Provider, Notifier và Widget
Mặc dù ba thành phần có mối liên hệ chặt chẽ, chúng không nên đảm nhận cùng một nhiệm vụ.
| Tiêu chí | Provider | Notifier | Widget |
|---|---|---|---|
| Mục tiêu | Khai báo nguồn dữ liệu và trạng thái | Quản lý và thay đổi trạng thái | Hiển thị giao diện |
| Xử lý logic nghiệp vụ | Có thể cung cấp hoặc kết hợp logic tạo dữ liệu | Thường là nơi đặt các phương thức nghiệp vụ thay đổi trạng thái | Nên hạn chế |
| Lưu trạng thái thay đổi | Phụ thuộc loại Provider và đối tượng liên kết | Trực tiếp quản lý trạng thái qua state |
Có thể có trạng thái cục bộ |
| Cập nhật giao diện | Gián tiếp thông qua cơ chế theo dõi | Phát ra trạng thái mới để hệ thống xử lý | Xây dựng lại giao diện khi cần |
| Khả năng tái sử dụng | Cao | Cao | Phụ thuộc cách thiết kế |
| Kiểm thử | Có thể kiểm tra nguồn dữ liệu và phụ thuộc | Có thể kiểm tra logic độc lập với UI | Thường cần kiểm thử giao diện hoặc tương tác |
Có thể hiểu rằng Provider là điểm truy cập của dữ liệu, Notifier là nơi quản lý cách dữ liệu thay đổi, còn Widget là nơi thể hiện dữ liệu đó cho người dùng.
Cách phân chia này không có nghĩa mọi logic đều phải đặt trong Notifier. Những thao tác truy cập dữ liệu phức tạp có thể được tách vào Repository hoặc Service, sau đó Notifier gọi đến các lớp đó. Mục tiêu là tạo ranh giới rõ ràng giữa giao diện, quản lý trạng thái và xử lý dữ liệu.
8. Phân biệt ref.watch, ref.read và ref.listen
Trong Riverpod, ref là đối tượng quan trọng giúp Widget hoặc Notifier tương tác với Provider. Ba phương thức thường được sử dụng là watch, read và listen.
8.1. ref.watch
ref.watch dùng để đọc và theo dõi giá trị của Provider.
Khi giá trị mà Widget theo dõi thay đổi, Widget có thể được xây dựng lại để hiển thị dữ liệu mới.
final cart = ref.watch(cartProvider);
Phương thức này phù hợp với các dữ liệu ảnh hưởng trực tiếp đến giao diện, chẳng hạn danh sách sản phẩm, số lượng hàng hoặc thông tin tài khoản.
8.2. ref.read
ref.read dùng để đọc giá trị hoặc truy cập đối tượng quản lý trạng thái mà không thiết lập cơ chế theo dõi từ vị trí gọi đó.
ref.read(cartProvider.notifier).clearCart();
Cách sử dụng phổ biến là gọi phương thức của Notifier trong callback, ví dụ khi người dùng nhấn nút xóa giỏ hàng.
Không nên thay ref.watch bằng ref.read chỉ để tránh Widget cập nhật lại. Nếu giao diện phụ thuộc vào dữ liệu thay đổi mà không theo dõi dữ liệu đó, giao diện có thể không hiển thị đúng trạng thái mới.
8.3. ref.listen
ref.listen dùng để phản ứng với sự thay đổi của Provider bằng một callback, thường phục vụ những tác vụ phụ như hiển thị thông báo, điều hướng hoặc ghi nhận sự kiện.
Ví dụ, một ứng dụng có thể lắng nghe trạng thái gửi đơn hàng. Khi đơn hàng được tạo thành công, ứng dụng hiển thị thông báo xác nhận.
ref.listen(orderProvider, (previous, next) {
next.whenOrNull(
data: (order) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đặt hàng thành công'),
),
);
},
);
});
Ví dụ này giả định orderProvider có trạng thái bất đồng bộ trả về một đối tượng đơn hàng. Trong ứng dụng thực tế, cần thiết kế thêm cơ chế phù hợp để tránh hiển thị lại thông báo ngoài ý muốn khi trạng thái dữ liệu được phát ra nhiều lần.
8.4. Bảng tổng hợp
| Phương thức | Mục đích | Trường hợp sử dụng |
|---|---|---|
ref.watch |
Theo dõi dữ liệu | Hiển thị danh sách, số lượng, tổng tiền |
ref.read |
Đọc dữ liệu hoặc gọi hành động | Nhấn nút, gửi biểu mẫu, gọi phương thức Notifier |
ref.listen |
Phản ứng với thay đổi | Hiển thị thông báo, điều hướng, xử lý sự kiện phụ |
Việc sử dụng đúng ba phương thức giúp mã nguồn rõ ràng và tránh những lần cập nhật giao diện không cần thiết.
9. Nguyên tắc thiết kế Provider và Notifier
9.1. Tách biệt trách nhiệm
Mỗi thành phần nên đảm nhận một nhiệm vụ rõ ràng. Widget tập trung vào giao diện, Notifier quản lý trạng thái và logic thay đổi trạng thái, còn Repository phụ trách giao tiếp với nguồn dữ liệu khi cần.
Ví dụ, Notifier quản lý danh sách sản phẩm không nên chứa quá nhiều mã HTTP, truy vấn SQL và thành phần giao diện trong cùng một lớp.
9.2. Hạn chế sửa đổi trạng thái trực tiếp
Với các trạng thái dạng danh sách hoặc đối tượng phức tạp, nên tạo giá trị mới khi có thay đổi thay vì chỉnh sửa trực tiếp cấu trúc đang được sử dụng.
Ví dụ:
state = [...state, newProduct];
Cách viết này tạo danh sách mới chứa các phần tử cũ và sản phẩm mới. Việc cập nhật theo cách có thể nhận diện rõ ràng giúp quản lý thay đổi dễ hơn.
Đối với trạng thái phức tạp hơn, có thể sử dụng các lớp dữ liệu bất biến hoặc thư viện hỗ trợ tạo đối tượng mới.
9.3. Không đặt toàn bộ logic trong Widget
Khi xử lý nghiệp vụ trực tiếp trong build(), mã nguồn có thể khó bảo trì và logic có nguy cơ bị thực thi lại khi Widget xây dựng lại.
Những hành động như thêm sản phẩm, xác thực thông tin hoặc gửi yêu cầu cập nhật dữ liệu nên được tổ chức trong các phương thức thích hợp. Widget gọi các phương thức đó khi người dùng tương tác.
9.4. Theo dõi đúng nguồn trạng thái
Chỉ sử dụng ref.watch cho những dữ liệu mà thành phần hiện tại thực sự cần phản ứng khi chúng thay đổi.
Nếu một Widget chỉ cần lấy đối tượng Notifier để gọi phương thức, ref.read thường phù hợp hơn. Ngược lại, khi giao diện cần phản ánh dữ liệu mới, phải theo dõi trạng thái tương ứng.
9.5. Phân chia Provider theo phạm vi sử dụng
Không nên tạo một Provider duy nhất chứa toàn bộ trạng thái của ứng dụng nếu những dữ liệu đó không có mối liên hệ chặt chẽ.
Ví dụ, giỏ hàng, thông tin xác thực, danh sách sản phẩm và bộ lọc tìm kiếm có thể được quản lý bởi những Provider riêng. Cách tổ chức này giúp giảm sự phụ thuộc giữa các tính năng và hỗ trợ bảo trì thuận tiện hơn.
9.6. Quản lý trạng thái lỗi
Những tác vụ bất đồng bộ cần có cách xử lý lỗi rõ ràng. Không nên chỉ hiển thị màn hình chờ mà không có phản hồi khi API thất bại.
Có thể sử dụng AsyncValue để phân biệt trạng thái tải, thành công và thất bại. Với những lỗi nghiệp vụ đặc thù, ứng dụng nên chuyển chúng thành thông báo phù hợp để người dùng hiểu tình trạng đang xảy ra.
9.7. Tổ chức kiểm thử
Notifier có thể được kiểm thử độc lập với giao diện. Lập trình viên có thể kiểm tra trạng thái ban đầu, hành vi thêm hoặc xóa sản phẩm, quy tắc xác thực và cách xử lý lỗi.
Ví dụ, đối với giỏ hàng, các trường hợp kiểm thử nên bao gồm:
-
Giỏ hàng ban đầu không có sản phẩm.
-
Thêm sản phẩm mới vào giỏ hàng.
-
Thêm lại sản phẩm đã tồn tại làm tăng số lượng.
-
Xóa sản phẩm theo mã.
-
Tính tổng tiền chính xác.
-
Xóa toàn bộ giỏ hàng.
-
Không cho phép số lượng giảm xuống dưới giới hạn quy định.
Kiểm thử giúp phát hiện lỗi trong logic quản lý trạng thái trước khi chúng ảnh hưởng đến giao diện người dùng.
10. Ưu điểm của Provider và Notifier
Tổ chức mã nguồn rõ ràng: Việc phân chia giữa nguồn dữ liệu, logic trạng thái và giao diện giúp dự án có cấu trúc dễ hiểu hơn.
Tái sử dụng trạng thái: Nhiều màn hình có thể theo dõi cùng một Provider mà không cần tạo ra các bản sao dữ liệu độc lập.
Hỗ trợ phát triển ứng dụng lớn: Các tính năng có thể được chia thành những Provider và Notifier riêng, giúp hạn chế sự phụ thuộc giữa các module.
Hỗ trợ bất đồng bộ: Riverpod cung cấp AsyncNotifier, FutureProvider và StreamProvider để quản lý những nguồn dữ liệu bất đồng bộ.
Dễ kiểm thử: Logic của Notifier có thể được kiểm tra mà không cần khởi chạy toàn bộ giao diện Flutter.
Quản lý phụ thuộc linh hoạt: Provider có thể tham chiếu đến Provider khác, giúp tổ chức các lớp dịch vụ, cấu hình và nguồn dữ liệu.
Cập nhật giao diện theo trạng thái: Các Widget theo dõi Provider có thể phản ánh những thay đổi dữ liệu một cách có hệ thống.
11. Hạn chế và những lưu ý khi sử dụng
Mặc dù Provider và Notifier mang lại nhiều lợi ích, việc sử dụng không đúng cách vẫn có thể khiến dự án trở nên phức tạp.
Thứ nhất, lập trình viên cần hiểu cách hoạt động của Provider, Notifier và các phương thức ref. Nếu sử dụng sai giữa watch, read và listen, giao diện có thể không được cập nhật như mong muốn hoặc các tác vụ phụ bị thực hiện không đúng thời điểm.
Thứ hai, khi số lượng Provider tăng lên, việc đặt tên, phân chia thư mục và quản lý sự phụ thuộc cần được thống nhất. Nếu không có quy ước rõ ràng, dự án sẽ khó tìm kiếm và bảo trì.
Thứ ba, Notifier không tự động giải quyết mọi vấn đề về lưu trữ dữ liệu. Trạng thái được quản lý trong bộ nhớ không đồng nghĩa dữ liệu sẽ tồn tại sau khi ứng dụng bị đóng. Nếu cần lưu dữ liệu lâu dài, ứng dụng phải kết hợp với cơ sở dữ liệu cục bộ hoặc dịch vụ lưu trữ thích hợp.
Thứ tư, cần lựa chọn API tương thích với phiên bản Riverpod của dự án. Những ví dụ sử dụng NotifierProvider và AsyncNotifierProvider cần có phiên bản thư viện hỗ trợ tương ứng.
Cuối cùng, không nên đưa toàn bộ nghiệp vụ vào một Notifier quá lớn. Khi một Notifier chịu trách nhiệm quá nhiều chức năng, lập trình viên nên xem xét tách thành những thành phần nhỏ hơn hoặc chuyển phần xử lý dữ liệu sang Repository và Service.
12. Ứng dụng thực tế trong dự án Flutter
Provider và Notifier có thể được áp dụng trong nhiều loại ứng dụng khác nhau.
| Chức năng | Provider và Notifier có thể đảm nhận |
|---|---|
| Đăng nhập | Quản lý trạng thái đăng nhập, thông tin người dùng và lỗi xác thực |
| Thương mại điện tử | Quản lý giỏ hàng, danh sách yêu thích, bộ lọc và đơn hàng |
| Quản lý công việc | Thêm, sửa, xóa và lọc công việc |
| Ứng dụng thể thao | Quản lý lịch tập, lịch sử tập luyện và mục tiêu cá nhân |
| Quản lý thành viên | Theo dõi thông tin hội viên, trạng thái gói tập và lịch sử điểm danh |
| Ứng dụng sử dụng API | Tải dữ liệu, hiển thị trạng thái chờ và xử lý lỗi |
| Ứng dụng thời gian thực | Theo dõi dữ liệu thay đổi liên tục thông qua Stream |
Đối với một dự án thương mại điện tử, có thể tổ chức cấu trúc thư mục theo hướng sau:
lib/
├── core/
│ ├── providers/
│ └── services/
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ └── auth_repository.dart
│ │ ├── providers/
│ │ │ └── auth_provider.dart
│ │ ├── notifiers/
│ │ │ └── auth_notifier.dart
│ │ └── screens/
│ │ └── login_screen.dart
│ ├── products/
│ │ ├── data/
│ │ ├── providers/
│ │ ├── notifiers/
│ │ └── screens/
│ └── cart/
│ ├── models/
│ ├── providers/
│ ├── notifiers/
│ └── screens/
└── main.dart
Đây là cấu trúc minh họa, không phải quy tắc bắt buộc của Riverpod. Một số dự án có thể đặt Provider và Notifier chung trong một file hoặc tổ chức theo từng module nghiệp vụ. Điều quan trọng là cách sắp xếp phải nhất quán và phù hợp với quy mô dự án.
Trong cấu trúc này, data/ có thể chứa Repository và các thành phần truy cập dữ liệu; providers/ khai báo nguồn trạng thái; notifiers/ chứa logic quản lý trạng thái; còn screens/ chứa giao diện.
Với dự án nhỏ, không cần tách quá nhiều thư mục ngay từ đầu. Khi số lượng tính năng tăng lên, có thể tiếp tục phân chia để giữ mã nguồn dễ quản lý.
13. So sánh Provider và Notifier với các phương pháp quản lý trạng thái khác
Provider và Notifier trong Riverpod không phải là lựa chọn duy nhất để quản lý trạng thái trong Flutter. Những phương pháp như setState, Provider package kết hợp ChangeNotifier và BLoC cũng được sử dụng phổ biến.
| Tiêu chí | setState | Provider + ChangeNotifier | Riverpod + Notifier |
|---|---|---|---|
| Phạm vi sử dụng | Trạng thái cục bộ của Widget | Trạng thái chia sẻ giữa nhiều Widget | Trạng thái cục bộ hoặc dùng chung thông qua hệ thống Provider |
| Cách cập nhật | Gọi setState() |
Gọi notifyListeners() |
Gán trạng thái mới cho state |
| Quản lý phụ thuộc | Chủ yếu trong cây Widget | Thường thông qua cơ chế cung cấp đối tượng | Có hệ thống Provider và ref |
| Bất đồng bộ | Tự tổ chức | Tự tổ chức hoặc kết hợp công cụ khác | Hỗ trợ các API như AsyncNotifierProvider |
| Khả năng kiểm thử | Phù hợp với trạng thái đơn giản | Có thể kiểm thử ChangeNotifier | Có thể kiểm thử Notifier và Provider |
| Mức độ tổ chức | Đơn giản, ít thành phần | Phù hợp với ứng dụng có trạng thái chia sẻ | Phù hợp với ứng dụng cần quản lý phụ thuộc có hệ thống |
setState vẫn là lựa chọn phù hợp cho những trạng thái chỉ phục vụ một Widget, chẳng hạn bật hoặc tắt một phần giao diện.
Provider package và ChangeNotifier là một phương pháp phổ biến, đặc biệt trong các dự án Flutter đã sử dụng thư viện Provider từ trước.
Riverpod với Notifier cung cấp cách tổ chức trạng thái khác, trong đó Provider được khai báo độc lập với vị trí Widget và hỗ trợ quản lý sự phụ thuộc rõ ràng hơn. Tuy nhiên, không có phương pháp nào phù hợp tuyệt đối với mọi dự án. Lựa chọn cần dựa trên quy mô, yêu cầu nghiệp vụ, khả năng bảo trì và kinh nghiệm của nhóm phát triển.
14. Kết luận
Provider và Notifier là hai thành phần quan trọng trong hệ sinh thái quản lý trạng thái của Flutter, đặc biệt khi sử dụng Riverpod. Provider đảm nhiệm vai trò khai báo và cung cấp nguồn dữ liệu hoặc đối tượng quản lý trạng thái, trong khi Notifier chịu trách nhiệm khởi tạo trạng thái, định nghĩa logic nghiệp vụ và cập nhật dữ liệu khi có hành động xảy ra.
Sự kết hợp giữa hai thành phần giúp ứng dụng phân chia rõ ràng trách nhiệm giữa dữ liệu, logic và giao diện. Thay vì để mỗi Widget tự quản lý và xử lý dữ liệu riêng biệt, ứng dụng có thể sử dụng những nguồn trạng thái chung, hỗ trợ tái sử dụng logic và giảm sự phụ thuộc giữa các màn hình.
Thông qua NotifierProvider, AsyncNotifierProvider, ref.watch, ref.read và ref.listen, lập trình viên có thể xây dựng những tính năng từ đơn giản như bộ đếm, giỏ hàng đến phức tạp hơn như đăng nhập, tải dữ liệu API và quản lý đơn hàng.
Để sử dụng hiệu quả, lập trình viên cần lựa chọn đúng loại Provider, phân chia trách nhiệm hợp lý, cập nhật trạng thái rõ ràng và kiểm thử logic nghiệp vụ. Khi được áp dụng đúng cách, Provider và Notifier giúp mã nguồn Flutter có cấu trúc tốt hơn, dễ mở rộng, dễ bảo trì và phù hợp với quá trình phát triển những ứng dụng có quy mô lớn.








Khoá học lập trình game con rắn cho trẻ em




