开启生长之旅!这是我参与「日新计划 12 月更文应战」的第6天,点击检查活动详情

前沿

说到 Flutter 的状况办理结构,咱们耳熟能详的有 ProviderRiverpodBloc 以及大名鼎鼎的 Getx 等,可谓是数不胜数。但要说其间哪一个更优异,那或许 一千个人有一千个哈姆雷特。今天咱们就来介绍其间的 Riverpod,它也是 Provider 的开发团队 dash-overflow.net 针对 Provider 存在的缺陷,从头开发规划的一个状况办理结构,由此可见其价值。

一、什么是状况办理?

在 Flutter 中,状况办理是指在运用中办理修正数据的进程。简略来说,便是更新页面中的数据。

Flutter 的状况办理办法有许多种,咱们能够依据自己的需求挑选适宜的办法来办理运用的状况。一些常见的状况办理办法包括:

  • 部分状况:部分状况是指只影响单个组件的状况。能够运用 Flutter 的 setState 办法来更新部分状况。
  • 大局状况:大局状况是指影响整个运用的状况。能够运用 Flutter 的 Provider 插件来办理大局状况。
  • 同享状况:同享状况是指影响多个组件的状况。能够运用 Flutter 的 InheritedWidget 来同享状况。

二、知道Riverpod

1. Rivepod插件介绍

Riverpod(即 Provider 的变位词)适用于 Flutter / Dart 的呼应式缓存结构。它能够主动获取、缓存、组合和从头核算网络恳求,一同能还为你处理过错。

Riverpod 经过供给一种新的、共同的办法来编写事务逻辑,创意来自 Flutter 的 Widget。Riverpod 在许多方面都与 Widget 相似,但在状况方面不同。

运用这种新办法,那些杂乱的特性大部分都是默许完结,咱们只需求关注 UI 即可。

例如下面的代码片段是运用 Riverpod 完结的 Pub.dev 运用的简化代码:

// 从pub.dev中获取package的列表
@riverpod
Future<List<Package>> fetchPackages(
  FetchPackagesRef ref, {
  required int page,
  String search = '',
}) async {
  final dio = Dio();
  // 取一个API。这里咱们运用的是package:dio,但咱们也能够运用其他任何东西
  final response = await dio.get(
    'https://pub.dartlang.org/api/search?page=$page&q=${Uri.encodeQueryComponent(search)}',
  );
  // 将JSON呼应解码为Dart类
  final json = response.data as List;
  return json.map(Package.fromJson).toList();
}

这段代码是“输入搜索”所需的悉数事务逻辑,经过 @riverpod 注解的办法生成 Riverpod 代码。

2. 开端运用

  1. pubspec.yaml 引进 package
name: my_app_name
environment:
  sdk: ">=2.17.0 <3.0.0"
  flutter: ">=3.0.0"
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.1.1
  riverpod_annotation: ^1.0.6
dev_dependencies:
  build_runner:
  riverpod_generator: ^1.0.6
  1. 运用比如:计数器
void main() {
  runApp(
    // 增加ProviderScope能够使Riverpod适用于整个项目
    const ProviderScope(child: MyApp()),
  );
}
/// provider是大局声明的,并指定怎么创立一个状况
final counterProvider = StateProvider((ref) => 0);
class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: Center(
        // Consumer是一个答应读取providers的widget
        child: Consumer(
          builder: (context, ref, _) {
            final count = ref.watch(counterProvider);
            return Text('$count');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // read办法用于更新provider的值
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

3. Riverpod和provider之间的联络

Riverpod 是在寻找处理 Provider 所面对的各种技术限制的计划时诞生的。开端,Riverpod 被认为是 Provider 中处理这一问题的一个主要版本。但开发团队终究还是决议不这么做,由于这将是一个适当大的突破性变化,而 Provider 是最常用的 Flutter 包之一。

尽管如此,Riverpod 和 Provider 在概念上还是适当相似的。两个包都扮演相似的人物。两者都企图:

  • 缓存并处理一些有状况的目标
  • 供给一种在测验期间模仿这些目标的办法
  • 为 Widgets 供给了一种简略的办法来监听这些目标。

Riverpod 修复了 Provider 的各种基本问题,例如但不限于:

  • 显著简化了 “Providers” 的组合。Riverpod 没有运用繁琐且简略出错的 ProxyProvider 东西,而是运用了简略且强壮的实用东西,例如 ref.watchref.listen
  • 答应多个 “provider” 揭露同一类型的值。这消除了在运用 int or String 等类型值时界说自界说类的需求。
  • 无需在测验中从头界说 providers。在 Riverpod 中,provider 默许状况下能够运用内部测验。
  • 经过供给处理目标的代替办法( autoDispose )来削减对“效果域”处理目标的过度依赖。尽管功能强壮,但确认供给者的效果域是适当高档的,并且很难做到正确。

4. 缺陷

Riverpod 仅有的缺陷是它需求改动 Widget类型 才能工作:

  • 在 Riverpod 中,你应该承继 ConsumerWidget ,而不是承继 StatelessWidget
  • 在 Riverpod 中,你应该承继 ConsumerStatefulWidget ,而不是承继 StatefulWidget

三、Providers

1. Provider

provier 是所有 providers 中的最根底的类。provider 通用用于:

  • 缓存核算
  • 向其他 providers 揭露一个值(例如Respository / HttpClient)
  • 为测验或 widget 供给掩盖值的办法
  • 在非必要状况下削减 providers / Widget 的 rebuild 次数

运用示例:

假设咱们的运用中有一个 StateNotifierProvider 来操作待办事项列表:

@riverpod
class Todos extends _$Todos {
  @override
  List<Todo> build() {
    return [];
  }
  void addTodo(Todo todo) {
    state = [...state, todo];
  }
}

咱们能够运用 Providertodos 的筛选列表,仅显现已完结的 todos:

@riverpod
List<Todo> completedTodos(CompletedTodosRef ref) {
  final todos = ref.watch(todosProvider);
  // 咱们只回来完结的todo
  return todos.where((todo) => todo.isCompleted).toList();
}

咱们的 UI 现在能够经过监听来显现已完结的待办事项列表 completedTodosProvider

Consumer(builder: (context, ref, child) {
  final completedTodos = ref.watch(completedTodosProvider);
  // TODO 运用ListView/GridView/...显现已完结列表
});

2. StateNotifierProvider

StateNotifierProvider 是一个用于监听和揭露 StateNotifier 的 providers(来自 Riverpod 从头导出的包 state_notifier)
StateNotifierProviderStateNotifier 一同是 Riverpod 引荐的用于办理状况的处理计划。
它一般用于:

  • 揭露一个不可变的状况,它能够在对自界说事情做出反响后随时刻改动。
  • 将修正某些状况的逻辑(也称为“事务逻辑”)集中在一个当地,跟着时刻的推移进步可保护性。

用法示例:

咱们能够 StateNotifierProvider 用来完结一个待办事项列表,例如 addTodo让 UI 修正与用户交互的待办事项列表:

// StateNotifier的状况应该是不可变的。
// 也能够运用像frozen这样的包来协助完结。
@immutable
class Todo {
  const Todo({required this.id, required this.description, required this.completed});
  // 类中的所有特点都应该是final。
  final String id;
  final String description;
  final bool completed;
  // 由于Todo是不可变的,咱们完结了一个办法,答应clone
  // 内容略有不同的Todo。
  Todo copyWith({String? id, String? description, bool? completed}) {
    return Todo(
      id: id ?? this.id,
      description: description ?? this.description,
      completed: completed ?? this.completed,
    );
  }
}
// 传递给StateNotifierProvider的StateNotifier类
// 这个类不该该在其"state"特点之外露出状况,这意味着
// 没有公共getter特点
// 这个类的public办法将答应UI修正状况
class TodosNotifier extends StateNotifier<List<Todo>> {
  // 将待办事项列表初始化为一个空列表
  TodosNotifier(): super([]);
  // 答应UI增加待办事项。
  void addTodo(Todo todo) {
    // 由于咱们的状况是不可变的,所以不答应履行' state.add(todo) '
    // 相反,咱们应该创立一个新的待办事项列表,其间包含曾经的项目和新的项目
    // 在这里运用Dart的打开运算符是有协助的
    state = [...state, todo];
    // 调用"state =" 不需求调用“notifyListeners”或相似的办法。
    // 会在必要时主动rebuild UI
  }
  // 答应删去待办事项
  void removeTodo(String todoId) {
    // 相同,咱们的状况是不可变的。所以咱们创立了一个新的列表,而不是改动现有的列表。
    state = [
      for (final todo in state)
        if (todo.id != todoId) todo,
    ];
  }
  // 把待办事项符号为已完结
  void toggle(String todoId) {
    state = [
      for (final todo in state)
        // 只符号已完结的匹配todo
        if (todo.id == todoId)
          // 相同,由于咱们的状况是不可变的,所以需求创立todo的副本					
      		// 运用之前完结的copyWith办法来协助完结
          todo.copyWith(completed: !todo.completed)
        else
          // other todos are not modified
          todo,
    ];
  }
}
// 最终,咱们运用StateNotifierProvider来答应UI与咱们的TodosNotifier类交互
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});

现在咱们现已界说了一个 StateNotifierProvider,咱们能够运用它与 UI 中的待办事项列表进行交互:

class TodoListView extends ConsumerWidget {
  const TodoListView({Key? key}): super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 当待办事项列表更改时从头构建Widget
    List<Todo> todos = ref.watch(todosProvider);
    // 在一个可翻滚的列表视图中呈现待办事项
    return ListView(
      children: [
        for (final todo in todos)
          CheckboxListTile(
            value: todo.completed,
            // 当点击待办事项时,更改其完结状况
            onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
            title: Text(todo.description),
          ),
      ],
    );
  }
}

3. FutureProvider

FutureProvider 等价于 Provier 但用于异步代码

FutureProvider 一般用于:

  • 履行弛缓存异步操作(例如网络恳求)
  • 很好地处理异步操作的过错/加载状况
  • 将多个异步值组合成另一个值

运用示例:读取装备

/// 界说FutureProvider
@riverpod
Future<Configuration> fetchConfigration(FetchConfigrationRef ref) async {
  final content = json.decode(
    await rootBundle.loadString('assets/configurations.json'),
  ) as Map<String, Object?>;
  return Configuration.fromJson(content);
}
/// 更新UI
Widget build(BuildContext context, WidgetRef ref) {
  final config = ref.watch(fetchConfigrationProvider);
  return config.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (config) {
      return Text(config.host);
    },
  );
}

4. StreamProvider

StreamProvider相似于 FutureProvider 但用于 Stream 而不是 Future

StreamProvider一般用于:

  • 听 Firebase 或网络套接字
  • 每隔几秒重建另一个供应商

运用 StreamProviderStreamBuilder 有许多好处:

  • 它答应其他 providers 运用 ref.watch 收听流
  • 得益于 AsyncValue,它确保正确处理加载和过错状况
  • 它消除了区别播送流和一般流的需求
  • 它缓存流宣布的最新值,确保假如在宣布事情后增加侦听器,侦听器仍然能够立即拜访最新的事情
  • 它答应经过掩盖 StreamProvider

5. StateProvider

StateProvider 是一个供给者,它揭露了一种修正其状况的办法。它是 StateNotifierProvider 的简化版,旨在防止为非常简略的用例编写 StateNotifier

StateProvider 存在的主要目的是答应 经过用户界面修正简略变量。
StateProvider 的运用一般是以下情形之一:

  • 枚举,例如过滤器类型
  • 字符串,一般是文本字段的原始内容
  • 布尔值,用于复选框
  • 数字,用于分页或年龄表单字段

假如呈现以下状况,则不该运用 StateProvider

  • 你的状况需求验证逻辑
  • 您的状况是一个杂乱的目标(例如自界说类、列表/地图……)
  • 修正状况的逻辑比简略的 count++.

运用示例:运用下拉菜单更改过滤器类型

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

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;
});

然后,用户界面能够经过履行以下操作来显现产品列表:

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} \$'),
        );
      },
    ),
  );
}

咱们能够增加一个下拉列表,这将答应按价格或按名称过滤咱们的产品:

// 表明筛选器类型的枚举
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(
      // ... 
    ),
  );
}

现在咱们有了一个下拉列表,让咱们创立一个下拉列表 StateProvider 并将其状况与咱们的 providers 同步。

// 创立StateProvider
final productSortTypeProvider = StateProvider<ProductSortType>(
  // 咱们回来默许的排序类型,这里是name。
  (ref) => ProductSortType.name,
);
// 经过以下办法将此供给程序与咱们的下拉列表连接起来
DropdownButton<ProductSortType>(
  // 当排序类型改动时,这将从头构建下拉列表以更新显现的图标。
  value: ref.watch(productSortTypeProvider),
  // 当用户与下拉菜单交互时,咱们更新供给者状况。
  onChanged: (value) =>
      ref.read(productSortTypeProvider.notifier).state = value!,
  items: [
    // ...
  ],
),
// 更新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));
  }
});

就这样!此更改足以让用户界面在排序类型更改时主动从头呈现产品列表。

6. ChangeNotifierProvider

ChangeNotifierProvider(仅限 flutter_riverpod / hooks_riverpod)是一个 provider,用于从 Flutter 本身监听和揭露 ChangeNotifier

Riverpod ChangeNotifierProvider 不鼓励运用,主要用于:

  • package:provider从运用其时的轻松过渡 ChangeNotifierProvider
  • 支撑可变状况,即便不可变状况是首选

运用可变状况而不是不可变状况有时会更有用。缺陷是,它或许更难保护并且或许会损坏各种功能。
例如,provider.select 假如你的状况是可变的,那么运用来优化你的 Widget 的重建或许不起效果,由于 select 会认为该值没有改动。
因而,运用不可变数据结构有时会更快。因而,针对您的用例拟定基准非常重要,以确保您经过运用 ChangeNotifierProvider

运用示例:

ChangeNotifierProvider 用来完结一个待办事项列表。这样做将答应咱们揭露办法,例如 addTodo 让 UI 修正用户交互的待办事项列表:

class Todo {
  Todo({
    required this.id,
    required this.description,
    required this.completed,
  });
  String id;
  String description;
  bool completed;
}
class TodosNotifier extends ChangeNotifier {
  final todos = <Todo>[];
  // 答应UI增加待办事项
  void addTodo(Todo todo) {
    todos.add(todo);
    notifyListeners();
  }
  // 答应删去待办事项
  void removeTodo(String todoId) {
    todos.remove(todos.firstWhere((element) => element.id == todoId));
    notifyListeners();
  }
  // 把待办事项符号为已完结
  void toggle(String todoId) {
    for (final todo in todos) {
      if (todo.id == todoId) {
        todo.completed = !todo.completed;
        notifyListeners();
      }
    }
  }
}
// 最终,运用StateNotifierProvider来答应UI与咱们的TodosNotifier类交互。
final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
  return TodosNotifier();
});

运用它与 UI 中的待办事项列表进行交互:

class TodoListView extends ConsumerWidget {
  const TodoListView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 当待办事项列表更改时从头构建widget
    List<Todo> todos = ref.watch(todosProvider).todos;
    // 在一个可翻滚的列表视图中呈现待办事项
    return ListView(
      children: [
        for (final todo in todos)
          CheckboxListTile(
            value: todo.completed,
            // 当点击待办事项时,更改其完结状况
            onChanged: (value) =>
                ref.read(todosProvider.notifier).toggle(todo.id),
            title: Text(todo.description),
          ),
      ],
    );
  }
}

四、关于Riverpod的代码生成

代码生成便是运用东西为咱们生成代码的思维。

在 Dart 中,它的缺陷是需求额外的步骤来“编译” 运用。尽管这个问题或许会在不久的将来得到处理,由于 Dart 团队正在研讨这个问题的潜在处理计划。

在 Riverpod 的上下文中,代码生成是关于稍微改动界说 “providers” 的语法。例如:

final fetchUserProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
  final json = await http.get('api/user/$userId');
  return User.fromJson(json);
});

运用代码生成,咱们会写:

@riverpod
Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
  final json = await http.get('api/user/$userId');
  return User.fromJson(json);
}

运用 Riverpod 时,代码生成是完全可选的。完全能够在没有的状况下运用 Riverpod。
一同,Riverpod 拥抱代码生成并引荐运用它。

五、关于Riverpod的钩子

Hooks” 是独立于 Riverpod 的通用实用 package: flutter_hooks。
尽管 flutter_hooks 是一个完全独立的包并且与 Riverpod 没有任何联络(至少没有直接联络),但一般将 Riverpodflutter_hooks 配对在一同。毕竟,Riverpod 和 flutter_hooks 是由同一个团队保护的。

钩子是完全可选的。你能够不必运用钩子,尤其是在你开端运用 Flutter 时。它们是强壮的东西,但不是很像 Flutter。因而,首先从一般的 Flutter / Riverpod 开端或许是有意义的,一旦你有了更多的经历,再回到钩子上。

什么是钩子?

钩子是在小部件内部运用的函数。它们被规划为 StatefulWidget 的代替品,以使逻辑更具可重用性和可组合性。

假如 Riverpod 的供给者用于“大局” 运用,则挂钩用于本地 widget。钩子一般用于处理有状况的 UI 目标,例如 TextEditingController、 AnimationController。 它们还能够作为“构建器”方式的代替品,用不触及“嵌套”的代替品代替,诸如 FutureBuilder / TweenAnimatedBuilder 之类的 Widget 明显进步可读性。

一般,钩子有助于:

  • 方式
  • 动画
  • 响运用户事情

运用示例:

class FadeIn extends HookWidget {
  const FadeIn({Key? key, required this.child}) : super(key: key);
  final Widget child;
  @override
  Widget build(BuildContext context) {
    // 创立一个AnimationController卸载Widget时,控制器将主动被处置。
    final animationController = useAnimationController(
      duration: const Duration(seconds: 2),
    );
    // useEffect适当于initState + didUpdateWidget + dispose
    // 传递给useEffect的回调在钩子第一次呈现时履行
    // 被调用时履行,然后在作为第二个参数传递的列表发生变化时履行
    // 由于咱们在这里传递了一个空的const列表,这严格地等同于' initState '。
    useEffect(() {
      // 在widget第一次呈现时启动动画。
      animationController.forward();
      // 咱们能够挑选在这里回来一些“dispose”逻辑
      return null;
    }, const []);
    // 告诉Flutter在动画更新时从头构建此Widget。
    // 这适当于AnimatedBuilder
    useAnimation(animationController);
    return Opacity(
      opacity: animationController.value,
      child: child,
    );
  }
}

总结

今天先介绍了状况办理的一些根底概念,然后介绍了 Riverpod 和 Provider 之间的联络和差异,最终着重介绍了 Riverpod 的一些用法和 6Provider,以及 Riverpod 的代码生成钩子。下一篇咱们将开端介绍Riverpod的特性和完结原理。