重学Riverpod(1/2)

1,248 阅读14分钟

4bf2a95bb3db3a74475ad4132dbd4912.png

「 入门指南 」

使用Riverpod,可以给我们的项目带来以下好处。官方总结的有5点:

  1. 允许在多个位置轻松访问该状态。
  2. 简化了将这种状态与其他状态结合
  3. 启用性能优化,说通俗点就是支持局部更新
  4. 提高应用程序的可测试性。有了提供商,您不需要复杂的setUp/tearDown步骤。
  5. 允许轻松集成高级功能,如日志记录或拉到刷新。

总结完优势之后,我们开启学习第一步。让我们从基础知识开始:安装Riverpod,然后写一个“你好世界”。

安装软件包

将以下内容添加到您的pubspec.yaml

  sdk: ">=2.17.0 <3.0.0"
  flutter: ">=3.0.0"


dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.3.6

然后运行flutter pub get

就是这样。您已将Riverpod添加到您的应用程序中!

使用示例:Hello world

现在我们已经安装了Riverpod, 我们可以开始使用它了。 以下片段展示了如何使用新依赖项来制作一个“你好世界”,三步即可体验

import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';

**(第一步)//我们创建一个“提供者”,它将存储一个值(这里是“Hello world”)。通过使用提供程序,我们可以模     拟覆盖暴露的值。**
final helloWorldProvider = Provider((_) => 'Hello world');

void main() {
  runApp(
  **(2//在runApp中使用“ProviderScope”小部件包裹全部的应用。**
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
  (**3//使用watch来监听变化**
    final String value = ref.watch(helloWorldProvider);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(value),
        ),
      ),
    );
  }
}

然后,您可以通过flutter run来执行。

这将在您的设备上呈现“Hello world”。

注意1:您必须在Flutter应用程序的根部添加ProviderScope,否则无法正常使用。

注意2:如果之前使用的StatefulWidget,则改为ConsumerStatefulWidget.

/// A [StatefulWidget] that can read providers.
abstract class ConsumerStatefulWidget extends StatefulWidget {
  /// A [StatefulWidget] that can read providers.
  const ConsumerStatefulWidget({super.key});

  @override
  // ignore: no_logic_in_create_state
  ConsumerState createState();

  @override
  ConsumerStatefulElement createElement() {
    return ConsumerStatefulElement(this);
  }
}

注意3:如果之前使用的StatelessWidget,则改为ConsumerWidget.

「 概念梳理 」

现在我们已经安装了Riverpod,让我们来谈谈“Providers”。他有好几种类型,之后会一一道来。。其实一个provider是一个对象,它封装了一段状态,并可以监听这个状态。 看完上面展示的hello world,混个眼熟,接下来我们开始详细介绍用法。

如何创建provider

供应商有很多变体,但它们的工作方式都是一样的。最常见的用法是将它们声明为全局常量,例如:

WechatIMG1.jpeg 这个片段由三个部分组成:

  • final myProvider,变量的声明。这个变量是我们未来用来读取提供商状态的变量。提供者应该始终是final
  • Provider, Provider是所有Providers中最基本的。它暴露了一个永远不会改变的对象。我们可以用StreamProviderStateNotifierProvider等其他替换Provider,以改变与值交互的方式。
  • 创建共享状态的函数。该函数将始终接收一个名为ref的对象作为参数。此对象允许我们读取其他providers,在provider的状态被销毁时执行一些操作,等等。

您可以毫无限制地声明任意数量的providers。与使用package:provider时不同,Riverpod允许创建多个提供商,以暴露相同“类型”的状态:

final cityProvider = Provider((ref) => 'London');
final countryProvider = Provider((ref) => 'England');

两个提供程序都创建String不会造成任何问题。

「6种不同类型的Providers」

每种Provider有各自的用法,有时很难理解何时使用一种提供商类型而不是另一种提供商类型。使用下表选择适合的provider用于widget。

类型Provider创建函数示例用例
Provider返回任何类型服务类/计算属性(过滤列表)
StateProvider返回任何类型过滤器条件/简单状态对象
FutureProvider返回任何类型的一个FutureAPI调用的结果
StreamProvider返回任何类型的流来自API的结果流
StateNotifierProvider返回StateNotifier的子类除非通过接口,否则是不可变的复杂状态对象
ChangeNotifierProvider返回ChangeNotifier的子类需要可变性的复杂状态对象

接下来详细讲一讲这些提供者

Provider

Provider是所有供应商中最基本的。它创造了一种价值......仅此而已

Provider通常用于:

  • 缓存计算
  • 将值暴露给其他providers(如aRepository/HttpClient)。
  • 提供一种测试或小部件覆盖值的方法。
  • 减少提供商/小部件的重建,而无需使用select,select用法之后会详细讲解,目前知道就行。

使用Provider缓存计算

Providerref.watch结合时,是缓存同步操作的强大工具。

一个例子是过滤一个todos列表。由于过滤列表可能略显消耗资源,因此我们最好不要在应用程序重新渲染时过滤我们的待办事项列表。在这种情况下,我们可以使用Provider为我们进行过滤。

为此,假设我们的应用程序有一个现有的StateNotifierProvider,它操作一个todos列表:

class Todo {
  Todo(this.description, this.isCompleted);
  final bool isCompleted;
  final String description;
}

class TodosNotifier extends StateNotifier<List<Todo>> {
  TodosNotifier() : super([]);

  void addTodo(Todo todo) {
    state = [...state, todo];
  }
  // TODO add other methods, such as "removeTodo", ...
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});

从那里,我们可以使用Provider公开过滤的todos列表,仅显示已完成的todos:

final completedTodosProvider = Provider<List<Todo>>((ref) {
  // 我们从todosProvider获取列表的所有todos
  final todos = ref.watch(todosProvider);

  // 在这里我们只筛选返回完成的todos
  return todos.where((todo) => todo.isCompleted).toList();
});

有了这个代码,我们的用户界面现在可以通过收听completedTodosProvider来显示已完成的todos列表,是不是很爽:

Consumer(builder: (context, ref, child) {
  final completedTodos = ref.watch(completedTodosProvider);
  // 展示todos 用于 ListView/GridView/...
});

有趣的是,列表过滤现在被缓存了。

这意味着,在添加/删除/更新todos之前,已完成的todos列表不会被重新计算,即使我们多次阅读已完成的todos列表。

请注意,当待办事项列表发生变化时,我们不需要手动使缓存失效。多亏了ref.watch,Provider能够自动知道何时必须重新计算结果。

使用Provider减少provider和小部件重建

Provider的一个独特方面是,即使重新计算了Provider(通常在使用ref.watch时),除非值发生变化,否则它也不会更新监听它的小部件。

举个例子: 启用/禁用分页视图的上一个/下一个按钮,当索引等0的时候,禁用上一个按钮

f8c38fad642e065389feb89b8ca41098.png

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

class PreviousButton extends ConsumerWidget {
  const PreviousButton({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // if not on first page, the previous button is active
    final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;

    void goToPreviousPage() {
      ref.read(pageIndexProvider.notifier).update((state) => state - 1);
    }

    return ElevatedButton(
      onPressed: canGoToPreviousPage ? goToPreviousPage : null,
      child: const Text('previous'),
    );
  }
}

上面代码的问题是,每当我们更改当前页面时,“上一个”按钮都会重建。
我们希望只有在激活和停用之间切换时才能重建按钮。问题的根源在于,我们正在计算是否允许用户直接在“上一个”按钮中转到上一页。 解决这个问题的一种方法是将此逻辑提取到小部件之外并提取到一个Provider

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

// 用于计算是否允许用户转到上一页
final canGoToPreviousPageProvider = Provider<bool>((ref) {
  return ref.watch(pageIndexProvider) != 0;
});

class PreviousButton extends ConsumerWidget {
  const PreviousButton({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 现在去watching新的Provider
    // 我们的小部件不再计算我们是否可以转到前一个页面。
    final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

    void goToPreviousPage() {
      ref.read(pageIndexProvider.notifier).update((state) => state - 1);
    }

    return ElevatedButton(
      onPressed: canGoToPreviousPage ? goToPreviousPage : null,
      child: const Text('previous'),
    );
  }
}

同学们仔细看看这两段代码的区别体会。 从现在开始,当页面索引发生变化时,我们的canGoToPreviousPageProvider将被重新计算。但是,如果provider暴露的值没有改变,那么PreviousButton将不会重建。

这一变化既改善了我们按钮的性能,又把逻辑处理提取到widget之外。

StateProvider

StateProvider存在主要是为了允许用户界面修改简单变量。这是StateNotifierProvider的简化,旨在避免为非常简单的用例编写StateNotifier类。

适合场景:

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

不适合场景:

  • 你的状态需要验证逻辑
  • 你的状态是一个复杂的对象(例如自定义类、列表/地图......)
  • 修改状态的逻辑比简单的count++更高级。

对于更高级的情况,请考虑使用StateNotifierProvider,并创建一个StateNotifier类。拥有自定义StateNotifier类对于项目的长期可维护性至关重要——因为它将状态的业务逻辑集中在一个地方。

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

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

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

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

// 枚举过滤类型
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,并将下拉菜单的状态与我们provider同步。

第一步创建StateProvider

final productSortTypeProvider = StateProvider<ProductSortType>(  
//我们返回默认的筛选类型,根据名字
(ref) => ProductSortType.name,  
);

第二步,我们可以通过以下方式将此提供商与我们的下拉列表连接

DropdownButton<ProductSortType>(
  value: ref.watch(productSortTypeProvider),
  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));
  }
});

仅此而已!此更改足以让用户界面在排序类型更改时自动重新呈现产品列表,是不是非常方便。

怎么样基于之前的值来更新state,而不需要重复去reading。

有时,您希望根据之前的值更新StateProvider的状态。当然,你最终可能会写:

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

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

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

虽然这个片段没有什么特别的问题,但语法有点不方便。 为了使语法更好一点,我们可以使用update功能。此函数将接受回调,该回调将接收当前状态,并预计将返回新状态。此更改实现了相同的效果,同时使语法更好一点

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

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

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

StateNotifierProvider

StateNotifierProvider是一个用于监听和公开StateNotifier的提供程序。StateNotifierProviderStateNotifier一起,是Riverpod推荐的管理状态的解决方案,这些状态可能会因用户交互而发生变化。

适合场景:

  • 暴露一个不可变的状态,在对自定义事件做出反应后,该状态可能会随着时间的推移而改变。
  • 将修改某些状态(又称“业务逻辑”)的逻辑集中在一个地方,随着时间的推移提高可维护性。

示例如下:我们可以使用StateNotifierProvider来实现待办事项列表。这样做将允许我们公开addTodo等方法,让UI修改用户交互的todos列表。

// StateNotifier的状态应该是不可变的。  
//我们也可以使用像Freezed这样的包来帮助实现。
@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是不可变的,我们实现了一个方法,允许克隆  
// 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属性!  
//这个类的公共方法将允许UI修改状态。
class TodosNotifier extends StateNotifier<List<Todo>> {
  // 我们将待办事项列表初始化为空列表
  TodosNotifier(): super([]);

  //  该方法允许UI添加待办事项。
  void addTodo(Todo todo) {
   //由于我们的状态是不可变的,所以我们不允许执行' state.add(todo) '。  
//相反,我们应该创建一个新的待办事项列表,其中包含前面的待办事项  
//条目和新条目。  
//在这里使用Dart的扩展运算符是有帮助的!
    state = [...state, todo];
**//不需要调用"notifyListeners"或类似的东西。调用"state ="  
//在需要时自动重建UI。**
  }

  // 删除 todos
  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)
        if (todo.id == todoId)
          todo.copyWith(completed: !todo.completed)
        else
          // 其他待办事项不修改
          todo,
    ];
  }
}

//最后,我们使用StateNotifierProvider来允许UI与之交互
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});

现在我们已经定义了一个StateNotifierProvider,我们可以使用它与UI中的todos列表进行交互:

class TodoListView extends ConsumerWidget {
  const TodoListView({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
   //当todo列表更改时重新构建小部件
    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),
          ),
      ],
    );
  }
}

到此StateNotifierProvider的🌰完毕,看到这是否能上手实操用一用了?

FutureProvider

FutureProvider相当于Privider,但适用于异步代码,FutureProviderref.watch结合时获得了很多收益。这种组合允许在某些变量发生变化时自动重新获取一些数据,确保我们始终拥有最新的值。

适合场景:

  • 执行和缓存异步操作(如网络请求)
  • 很好地处理异步操作的错误/加载状态
  • 多个异步值组合成另一个值

📢FutureProvider不提供在用户交互后直接修改计算的方法。它旨在解决简单的用例。
对于更高级的场景,请考虑使用StateNotifierProvider。

举个🌰读取配置文件

FutureProvider可以很容易的通过读取JSON文件,创建Configuration对象。

final configProvider = FutureProvider<Configuration>((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) {
  AsyncValue<Configuration> config = ref.watch(configProvider);

  return config.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (config) {
      return Text(config.host);
    },
  );
}

当Future完成后,这将自动重建用户界面。同时,如果多个小部件需要这个Configuration,asset将仅被解码一次。

如您所见,在小部件中监听FutureProvider会返回一个AsyncValue——它允许处理错误/加载状态。

StreamProvider

StreamProviderFutureProvider相似,但适用于Streams而不是Futures

适合场景:

  • 收听Firebase或web-sockets
  • 每隔几秒钟重建另一个供应商

使用StreamProvider而不是StreamBuilder有很多好处:

  • 它允许其他提供商使用ref.watch收听流。
  • 多亏了AsyncValue,它确保了加载和错误案例得到正确处理。
  • 它消除了将广播流与普通流区分开来的必要性。
  • 它缓存流发出的最新值,确保如果在事件发出后添加alistener,侦听器仍然可以立即访问最新的事件。
  • 它允许通过覆盖StreamProvider在测试期间轻松模拟流。

使用示例:使用套接字进行实时聊天

final chatProvider = StreamProvider<List<String>>((ref) async* {
  // Connect to an API using sockets, and decode the output
  final socket = await Socket.connect('my-api', 4242);
  ref.onDispose(socket.close);
  
  var allMessages = const <String>[];
  await for (final message in socket.map(utf8.decode)) {
    // A new message has been received. Let's add it to the list of all messages.
    allMessages = [...allMessages, message];
    yield allMessages;
  }
});

然后,用户界面可以像这样收听实时流媒体聊天:

Widget build(BuildContext context, WidgetRef ref) {
  final liveChats = ref.watch(chatProvider);
  return liveChats.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stackTrace) => Text(error.toString()),
    data: (messages) {
      return ListView.builder(
        reverse: true,
        itemCount: messages.length,
        itemBuilder: (context, index) {
          final message = messages[index];
          return Text(message);
        },
      );
    },
  );
}

ChangeNotifierProvider

Riverpod不鼓励使用ChangeNotifierProvider