Quản lý state bằng Provider trong Flutter
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
, ChangeNotifier
encapsulate 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ứcbuild
instance
củaChangeNotifier
child
: dùng để tối ưu trong trường hợp phía dướiConsumer
bạn có 1widget
rất lớn nhưng nó lại không cần thiết phải bị thay đổi khi mà phíamodel
thông báo, thì ta có thểconstruct
nó chỉ lần thôi và ở hàmbuilder
thì 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 ClearCart
Consumer<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();