[官文翻译]Flutter状态管理库Riverpod - 概念 - 绑定 Provider 状态

523 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情


Riverpod的官方文档有多国语言,但是没有汉语,所以个人简单翻译了一版。

官网文档:Riverpod

GitHub:GitHub - rrousselGit/river_pod

Pub:riverpod | Dart Package (flutter-io.cn)

译时版本:riverpod 1.0.3


绑定 Provider 状态

阅读该指南之前,确保首先 阅读关于 Provider 的内容 。

在该指南中,我们会学习绑定 provider 状态的有关内容。

绑定 provider 状态

前面我们已经看到如何创建简单的 provider 。但是现实是,在一些情况下,一个 provider 可能想要读取另外一个 provider 的状态。

要做到这一点,可以使用传递给 provider 回调的 ref 对象,然后用它的 watch 方法。

作为示例,考虑下面的 provider :

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

现在可以创建另外一个 provider 消费  cityProvider

final weatherProvider = FutureProvider((ref) async {
  // 使用 `ref.watch` 监听另外一个 provider ,然后传递给它想要消费的 provider 。在这里是:cityProvider
  final city = ref.watch(cityProvider);

  // 然后就可以使用这个结果,基于 `cityProvider` 做一些事情。
  return fetchWeather(city: city);
});

就是这样。我们就创建了依赖其它 provider 的一个 provider 。

常见问题

监听的值随着时间改变?

根据监听的 provider 不同,值的获取可能随着时间改变。例如,可能在监听 StateNotifierProvider,或者监听的 provider 可能用 ProviderContainer.refresh/ref.refresh 强制刷新。

使用 watch 时,Riverpod 能够检测监听的值发生了改变并且在需要时自动地重新执行 provider 的创建回调。

这对于计算过的状态有用。例如,考虑 StateNotifierProvider 暴露了一个 TODO 列表:

class TodoList extends StateNotifier<List<Todo>> {
  TodoList(): super(const []);
}

final todoListProvider = StateNotifierProvider((ref) => TodoList());

一个常用的使用场景会是 UI 过滤 TODO 列表只表示完成/未完成的 TODO 。

实现这样的场景的简单方式会是:

  • 创建 StateProvider ,它会暴露当前选中的过滤方法:

    enum Filter {
      none,
      completed,
      uncompleted,
    }
    
    final filterProvider = StateProvider((ref) => Filter.none);
    
  • 创建独立的 provider 绑定过滤方法和 TODO 列表来暴露过滤后的 TODO 列表:

    final filteredTodoListProvider = Provider<List<Todo>>((ref) {
      final filter = ref.watch(filterProvider);
      final todos = ref.watch(todoListProvider);
    
      switch (filter) {
        case Filter.none:
          return todos;
        case Filter.completed:
          return todos.where((todo) => todo.completed).toList();
        case Filter.uncompleted:
          return todos.where((todo) => !todo.completed).toList();
      }
    });
    

然后,UI 可以监听 filteredTodoListProvider 来监听过滤后的 TODO 列表。 使用这样的方式,过滤器或者 TODO 列表发生改变时,UI 会自动更新。

想要参考这种方式在实践中的用法,可以看下 Todo List example 的源代码。

信息

该行为不是 Provider 特有的,所有的 provider 都可如此工作。

例如,可能会用  FutureProvider 绑定 watch 来实现支持 实时-配置 改变的查找 Future 。:

// 当前的查找过滤器
final searchProvider = StateProvider((ref) => '');

/// 配置可随时改变
final configsProvider = StreamProvider<Configuration>(...);

final charactersProvider = FutureProvider<List<Character>>((ref) async {
  final search = ref.watch(searchProvider);
  final configs = await ref.watch(configsProvider.future);
  final response = await dio.get('${configs.host}/characters?search=$search');

  return response.data.map((json) => Character.fromJson(json)).toList();
});

该代码会从 sevice 获取字符列表,然后当配置改变或者查找处理改变时会自动重新获取列表。

能否不监听 provider 而对其进行读取?

有时候,想要读取 provider 的内容,但是不会当获取的值发生改变时重新创建暴露的值。

Repository 是示例的一种,它从另外一个 provider 读取用于授权的用户令牌。 我们可以使用 watch 并在用户令牌改变时创建一个新的 Repository ,但是这样做几乎没有用处。

这种情况下,可以使用 read ,它和 watch 类似,但是当获取的值改变时,不会造成 provider 重建它暴露的值。

这种情况下,一个常用的实践是传递 ref.read 给创建的对象。创建的对象之后就能在任何需要的时候读取 provider 。

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
  Repository(this.read);

  /// `ref.read` 函数
  final Reader read;

  Future<Catalog> fetchCatalog() async {
    String token = read(userTokenProvider);

    final response = await dio.get('/path', queryParameters: {
      'token': token,
    });

    return Catalog.fromJson(response.data);
  }
}

注意

在工程中也可以传递 ref 代替 ref.read

final repositoryProvider = Provider((ref) => Repository(ref));

class Repository {
  Repository(this.ref);

  final Ref ref;
}

传递 ref.read 带来的唯一区别是它稍微简洁一些,并且能确保工程从不使用 ref.watch

严禁 在 provider 的函数体内部调用 READ 

final myProvider = Provider((ref) {
  // Bad practice to call `read` here
  final value = ref.read(anotherProvider);
});

如果使用 read 来试图避免期望外的对象重新构建,参考 我的 provider 更新如此频繁,怎么办?

How to test an object that receives read as a parameter of its constructor?

如何测试接收 read 作为构造方法参数的对象?

如果你正在使用 能否不监听 provider 而对其进行读取? 中描述的模式,可能想知道如何为对象编写测试。

这种场景下,考虑直接测试 provider 而不是测试原始对象。可以使用 ProviderContainer 类做到这一点:

final repositoryProvider = Provider((ref) => Repository(ref.read));

test('fetches catalog', () async {
  final container = ProviderContainer();
  addTearOff(container.dispose);

  Repository repository = container.read(repositoryProvider);

  await expectLater(
    repository.fetchCatalog(),
    completion(Catalog()),
  );
});

provider 更新过于频繁,怎么办?

如果对象频繁重新创建,那 provider 有可能在监听它并不关心的对象。

例如,可能正在监听一个 Configuration 对象,但是只用了 host 属性。

监听整个 Configuration 对象的话,如果 host 之外的属性发生了改变,也会导致 provider 会被重新评估 - 这不是所期望的。、

该问题的解决方案是创建一个独立的 provider ,只暴露 Configuration 中需要的属性(如 host ):

避免 监听整个对象:

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // 如果 configuration 发生了任何改变, 都会导致 productsProvider 重新获取 product 。
  final configs = await ref.watch(configProvider.future);

  return dio.get('${configs.host}/products');
});

参考 只需要对象的单个属性时使用 select :

final configProvider = StateProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // 只监听 host 。如果 configuration 中的其它内容发生了改变,不会无意义地重新评估 provider 。
  final host = await ref.watch(configProvider.select((config) => config.host));

  return dio.get('$host/products');
});

这只会在 host 发生改变时重新构建 productsProvider 。