一起养成写作习惯!这是我参与「掘金日新计划 · 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
。