I. Lời nói đầu
Có rất nhiều cách quản lý state trong Flutter. Sử dụng provider là 1 trong những cách đó. Chúng ta sẽ tìm hiểu thử xem sử dụng nó như thế nào.
II. Case study
Giả sử ta có 1 ứng dụng đơn giản. Ứng dụng này gồm 2 thành phần chính là 1 thư viện dùng để hiện thị danh sách các sản phẩm và 1 giỏ mua hàng để hiển thị danh sách những sản phẩm đã được chọn.
Từ 2 thành phần này, ta sẽ thiết kế 1 app bằng flutter có cấu trúc như hình bên dưới.

Vì vậy, chúng ta có ít nhất 5 lớp con của Widget. Trong cấu trúc này, ta thấy rằng có rất nhiều thành phần có nhu cầu truy cập vào state của thành phần khác. Ví dụ như mỗi MyListItem có nhu cầu truy cập vào state của MyCart để add chính nó vào MyCart.
Câu hỏi đặt ra là chúng ta sẽ đặt state ở đâu?
III. Đặt state ở phía trên
Ta nên đặt state ở trên Widget mà sử dụng state đó. Tại sao?
Trong declarative frameworks như Flutter, nếu bạn muốn update UI, bạn phải build phần UI đó lại. Sẽ không dễ dàng nếu bạn sử dụng giải pháp đại loại như là MyCart.updateWith (somethingNew). Nói cách khác, Và ngay cả khi bạn có thể làm cho điều này thành công, bạn đang chống lại frameworks thay vì để nó giúp bạn.
// BAD: DO NOT DO THIS
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
Ngay cả khi code phía trên hoạt động, bạn sẽ phải xử lý thêm phần phía bên dưới nữa:
// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
return SomeWidget(
// The initial state of the cart.
);
}
void updateWith(Item item) {
// Somehow you need to change the UI from here.
}
Bạn sẽ cần phải xem xét trạng thái hiện tại của UI và áp dụng dữ liệu mới cho nó. Thật khó để tránh lỗi theo cách này.
Trong Flutter, bạn tạo một widget mới mỗi khi nội dung của nó thay đổi. Thay vì sử dụng việc gọi phương thức thông thường như MyCart.updateWith (somethingNew), thì bạn nên sử dụng 1 constructor như MyCart (contents).
Vì bạn chỉ có thể khởi tạo các widget con bên trong phương thức build của parent Widget, nên nếu bạn muốn thay đổi contents, bạn cần đặt state ở MyCart’s parent hoặc cao hơn.
// GOOD
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
Trong ví dụ này, contents cần đặt ở MyApp. Bất cứ khi nào có sự thay đổi, MyCart được phía trên build lại. Do đó, MyCart không cần phải lo lắng về vòng đời — nó chỉ khai báo những gì sẽ hiển thị cho bất kỳ contents nhất định nào. Khi contents thay đổi, MyCart cũ sẽ biến mất và được thay thế hoàn toàn bằng MyCart mới.

IV. Truy cập state
Khi người dùng nhấp vào một trong các mặt hàng trong danh mục, mặt hàng đó sẽ được thêm vào giỏ hàng. Nhưng vì giỏ hàng nằm trên MyListItem, làm cách nào để làm điều đó?
Một giải pháp đơn giản là dùng callback mà MyListItem có thể gọi khi nó được nhấp vào.
@override
Widget build(BuildContext context) {
return SomeWidget(
// Construct the widget, passing it a reference to the method above.
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('user tapped on $item');
}
Các xử lý này hoạt động đúng, nhưng có 1 vấn đề là khi làm việc với state thì sẽ có rất nhiều nơi khác nhau muốn thay đổi state đó, điều này sẽ có nghĩa là ta sẽ cần rất nhiều callback tương ứng với việc có bao nhiêu chỗ muốn thay đổi state đó.
May mắn thay, trong Flutter ta có thể sử dụng Provider
Để sử dụng nó, đầu tiên ta cần thêm config ở file pubspec.yaml
name: my_name
description: Blah blah blah.
# ...
dependencies:
flutter:
sdk: flutter
provider: ^4.0.0
dev_dependencies:
# ...
Khi sử dụng Provider ta không cần phải lo lắng về callback nữa. Để hiểu được về Provider ta cần nắm 3 khái niệm quan trọng đó là ChangeNotifier, ChangeNotifierProvider và Consumer
V. ChangeNotifier
ChangeNotifier cấp cấp thông báo về sự thay đổi cho những listeners của nó. Nói cách khác, nếu ta có 1 ChangeNotifier thì ta có thể subscribe những sự thay đổi mà nó thông báo.
Trong là 1 cách để provider, ChangeNotifierencapsulate application state. Ta cần 1 hay nhiều ChangeNotifier tùy vào độ phức tạp của ứng dụng.
class CartModel extends ChangeNotifier {
/// Internal, private state of the cart.
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all items (assuming all items cost $42).
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This and [removeAll] are the only ways to modify the
/// cart from the outside.
void add(Item item) {
_items.add(item);
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}
Ta cần gọi phương thức notifyListeners ở phía model trong những thân hàm gây ra những sự thay đổi mà ta muốn phía UI update những sự thay đổi đó.
VI. ChangeNotifierProvider
ChangeNotifierProvider chẳng qua là một widget dùng để tạo ra instance của ChangeNotifier
Như bạn đã biết, ta cần đặt ChangeNotifierProvider phía trên những widget mà nó muốn truy cập. Vì vậy ta cần ở vị trí trên cả MyCart vàMyCatalog. Bạn sẽ muốn đặt nó ở vị trí qua hơn quá mức cần thiết, làm như vậy sẽ làm cho scope không được clean
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: MyApp(),
),
);
}
Trong trường hợp có nhiều ChangeNotifierProvider, ta sử dụng dụng cú pháp như bên dưới
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: MyApp(),
),
);
}
VII. Consumer
Giờ ta chỉ cần sử dụng những thứ đã chuẩn bị phía trên bằng Consumer
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);
Lưu ý là cần viết rõ kiểu dữ liệu cho Consumer thì Provider mới hoạt động được.
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
child,
Text("Total price: ${cart.totalPrice}"),
],
),
// Build the expensive widget here.
child: SomeExpensiveWidget(),
);
Consumer có 1 phương thức duy nhất là builder.
Builder nhận vào 3 tham số:
context: cũng là thứ cần thiết cho mọi phương thứcbuildinstancecủaChangeNotifierchild: dùng để tối ưu trong trường hợp phía dướiConsumerbạn có 1widgetrất lớn nhưng nó lại không cần thiết phải bị thay đổi khi mà phíamodelthông báo, thì ta có thểconstructnó chỉ lần thôi và ở hàmbuilderthì chỉ việc dùng lại
Tốt nhất là nên đặt Consumer ở phía thấp nhất có thể.
// DON'T DO THIS
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
// DO THIS
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
VIII. Provider.of
Đôi lúc ta không muốn data trong model làm thay đổi UI. Ví dụ như ta có nút sẽ xóa toàn bộ data của giỏ hàng nhưng lại không cần phải hiển thị giỏ hàng đó lên. Để tránh lãng phí, thay vì sử dụng ClearCartConsumer<CartModel> (làm cho rebuild widget mà không cần thiết phải rebuild). Thì ta có thể sử dụng code như bên dưới:
Provider.of<CartModel>(context, listen: false).removeAll();
