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);
        },
      ),
    );
  }
}
这样就实现了相同的效果,而且使语法更好一些。