跳到主要内容

StateProvider

StateProvider是一个公开了一种修改其状态的方法的provider。 它是 StateNotifierProvider 的简化版,旨在避免为非常简单的用例编写 StateNotifier 类。

StateProvider 的存在主要是为了允许用户界面对简单的变量进行修改。 所以StateProvider 的状态通常为:

  • 枚举类型,例如筛选器类型
  • 一段字符串(String),通常是输入框的原始内容
  • 用于复选框的布尔类型
  • 用于分页或年龄表单字段的数字

你不应该使用 StateProvider 如果:

  • 你的状态需要验证逻辑
  • 你的状态是一个复杂的对象 (比如自定义的类, 集合……)
  • 修改状态的逻辑比简单的 count++ 更复杂

对于更复杂的情况,可以考虑使用 StateNotifierProvider ,并创建一个 StateNotifier 类。 虽然最初的样板文件会有点大, 但有一个自定义的 StateNotifier 类对于项目的长期可维护性是至关重要的, 因为它将状态的业务逻辑集中在了一个地方。

使用示例:使用下拉菜单更改筛选类型

StateProvider 的一个真实的用例是管理简单表单组件的状态,比如下拉菜单/输入框/复选框。 特别来说,我们将看到如何使用 StateProvider 实现一个下拉菜单, 该下拉菜单允许更改产品列表的排序方式。

为了简单起见,我们将在应用中直接构建将获得的产品列表,如下所示:


class Product {
Product({required this.name, required this.price});

final String name;
final double price;
}

final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];

final productsProvider = Provider<List<Product>>((ref) {
return _products;
});

在真实的应用程序中,我们通常会使用 FutureProvider 通过网络请求来获得该列表。

然后,可以在用户界面上通过下面的操作来显示产品列表:


Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}

现在我们已经完成了基础,我们可以添加一个下拉菜单, 它将按价格或名称过滤我们的产品。 为此,我们将使用DropDownButton


// An enum representing the filter type
enum ProductSortType {
name,
price,
}

Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ... /* SKIP */
itemBuilder: (c, i) => Container(), /* SKIP END */
),
);
}

现在我们有了一个下拉列表, 让我们创建一个 StateProvider 并将下拉菜单的状态与我们的provider同步。

首先,让我们创建一个 StateProvider


final productSortTypeProvider = StateProvider<ProductSortType>(
// We return the default sort type, here name.
(ref) => ProductSortType.name,
);

然后,我们可以通过下面的操作将这个provider与我们的下拉菜单连接起来:

DropdownButton<ProductSortType>(
// When the sort type changes, this will rebuild the dropdown
// to update the icon shown.
value: ref.watch(productSortTypeProvider),
// When the user interacts with the dropdown, we update the provider state.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),

有了这些,我们现在应该能够更改筛选的类型。 不过它对产品列表没有影响! 现在是最后一部分:更新我们的 productsProvider 以对产品列表进行排序。

实现这一点的关键是使用 ref.watch, 让我们的 productsProvider 获得排序类型, 并在排序类型更改时重新计算产品列表。

代码实现会是:


final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});

就是这样!这个变改足以让用户界面在排序类型更改时自动重绘产品列表。

下面是在Dartpad上完整的例子:

如何在不读取provider两次的情况下根据之前的值更新状态

有时你希望根据之前的值更新 StateProvider 的状态。 自然而然,你可能会这样写:


final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// We're updating the state from the previous value, we ended-up reading
// the provider twice!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}

虽然这段代码没有什么特别的错误,但是语法上着实有点不太方便。

为了更方便地使用,我们可以使用 update 函数。 这个函数将接受一个回调函数,该回调函数将接收当前状态并返回新状态。 我们可以使用它来重构之前的代码:


final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).update((state) => state + 1);
},
),
);
}
}

这样就实现了相同的效果,而且使语法更好一些。