跳到主要内容

Provider

在所有的provider中 Provider 是最基础的。它创造了一个值……差不多就是这样。

Provider 一般用在:

  • 缓存计算。
  • 向其他provider(比如Repository/HttpClient)暴露一个值。
  • 为测试或widget提供重写值的方法。
  • 减少 provider/widget 的重新构建,不必使用 select

使用 Provider 缓存计算

Provider 是与 ref.watch 结合使用时用于缓存同步操作的强大的工具。

例如筛选待办清单。 由于筛选列表的性能开销可能略高,所以理想情况下,当应用重新渲染时我们不希望再筛选一遍待办列表。 在这种情况,我们可以使用 Provider 为我们进行筛选。

为此,假设我们的应用程序有一个 StateNotifierProvider ,它操作待办清单的列表:


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


class Todos extends _$Todos {

List<Todo> build() {
return [];
}

void addTodo(Todo todo) {
state = [...state, todo];
}
}

接着,我们可以使用 Provider 暴露经过筛选的待办清单列表,只显示完成的待办事项:



List<Todo> completedTodos(CompletedTodosRef ref) {
final todos = ref.watch(todosProvider);

// 我们只返回完成的待办事项
return todos.where((todo) => todo.isCompleted).toList();
}

使用这段代码,我们的UI现在可以通过监听 completedTodosProvider 来显示完成的待办清单列表:

Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO show the todos using a ListView/GridView/.../* SKIP */
return Container();
/* SKIP END */
});

有趣的是,这个筛选列表现在被缓存起来了。

这也就意味着不管我们阅读多少次已经完成的待办清单,只要在添加/删除/更新待办清单之前, 这个筛选的列表也不会被重新计算。

注意,当待办清单列表发生更改时我们不需要手动使缓存失效。多亏了 ref.watchProvider能够自动知道什么时候应该重新计算结果。

使用Provider 减少provider/widget的重新构建

Provider 独特的地方在于,就算在重新计算 Provider 时(通常在使用ref.watch时), 它也不会更新监听它的widget/provider,除非当中的值发生了变化。

一个真实的例子是启用/禁用分页视图中的上一个/下一个按钮:

stepper example

在这个例子中,我们特别关注在这个“previous”按钮。 这个按钮将用widget实现,它获取当前页面的索引,如果索引为0时,我们将禁用这个按钮。

这段代码可以是:



class PageIndex extends _$PageIndex {

int build() {
return 0;
}

void goToPreviousPage() {
state = state - 1;
}
}

class PreviousButton extends ConsumerWidget {
const PreviousButton({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// 如果不是第一页,那么前一页按钮可用
final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).goToPreviousPage();
}

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

这段代码的问题是,每当我们更改当前页面时,“previous”按钮将重新构建。 在理想情况下,我们希望按钮仅在激活和停用之间更改时重新构建。

但问题的根源在于我们在build函数内计算是否允许用户在“previous”按钮内直接转到上一页。

为了解决这个问题,我们将这个逻辑从widget中提取出来放到provider里面:



class PageIndex extends _$PageIndex {

int build() {
return 0;
}

void goToPreviousPage() {
state = state - 1;
}
}

// 一个计算是否允许用户跳转到上一页的provider

bool canGoToPreviousPage(CanGoToPreviousPageRef ref) {
return ref.watch(pageIndexProvider) != 0;
}

class PreviousButton extends ConsumerWidget {
const PreviousButton({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// 现在我们观察我们新的provider,
// 当我们跳转到前一页时我们的widget不再需要计算。
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).goToPreviousPage();
}

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

通过这样的小重构,多亏了 Provider 我们的 PreviousButton widget 当页面索引变化时将不再重新构建。

从现在开始,当页面索引改变时,我们的 canGoToPreviousPageProvider 将被重新计算。 但是如果provider暴露的值没有改变,那么 PreviousButton 将不会重新构建。

This change both improved the performance of our button and had the interesting benefit of extracting the logic outside of our widget. 这一更改提高了按钮的性能,还有一个有趣的好处,就是将逻辑抽离到widget之外。