주요 콘텐츠로 건너뛰기

Provider

주의

The content of this page may be outdated.
It will be updated in the future, but for now you may want to refer to the content in the top of the sidebar instead (introduction/essentials/case-studies/...)

Provider is the most basic of all providers. It creates a value... And that's about it.

Provider is typically used for:

  • caching computations
  • exposing a value to other providers (such as a Repository/HttpClient).
  • offering a way for tests or widgets to override a value.
  • reducing rebuilds of providers/widgets without having to use select.

Using Provider to cache computations

Provider is a powerful tool for caching synchronous operations when combined with ref.watch.

An example would be filtering a list of todos. Since filtering a list could be slightly expensive, we ideally do not want to filter our list of todos whenever our application re-renders. In this situation, we could use Provider to do the filtering for us.

For that, assume that our application has an existing NotifierProvider which manipulates a list of todos:


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];
}
// TODO add other methods, such as "removeTodo", ...
}

From there, we can use Provider to expose the filtered list of todos, showing only the completed todos:



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

// we return only the completed todos
return todos.where((todo) => todo.isCompleted).toList();
}

With this code, our UI is now able to show the list of the completed todos by listening to completedTodosProvider:

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

The interesting part is, the list filtering is now cached.

Meaning that the list of completed todos will not be recomputed until todos are added/removed/updated, even if we are reading the list of completed todos multiple times.

Note how we do not need to manually invalidate the cache when the list of todos changes. Provider is automatically able to know when the result must be recomputed thanks to ref.watch.

Reducing provider/widget rebuilds by using Provider

A unique aspect of Provider is that even when Provider is recomputed (typically when using ref.watch), it will not update the widgets/providers that listen to it unless the value changed.

A real world example would be for enabling/disabling previous/next buttons of a paginated view:

stepper example

In our case, we will focus specifically on the "previous" button. A naive implementation of such button would be a widget which obtains the current page index, and if that index is equal to 0, we would disable the button.

This code could be:



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) {
// if not on first page, the previous button is active
final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;

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

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

The issue with this code is that whenever we change the current page, the "previous" button will rebuild.
In the ideal world, we would want the button to rebuild only when changing between activated and deactivated.

The root of the issue here is that we are computing whether the user is allowed to go to the previous page directly within the "previous" button.

A way to solve this is to extract this logic outside of the widget and into a Provider:



class PageIndex extends _$PageIndex {

int build() {
return 0;
}

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

// A provider which computes whether the user is allowed to go to the previous page

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

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


Widget build(BuildContext context, WidgetRef ref) {
// We are now watching our new Provider
// Our widget is no longer calculating whether we can go to the previous page.
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

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

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

By doing this small refactoring, our PreviousButton widget will no longer rebuild when the page index changes thanks to Provider.

From now on when the page index changes, our canGoToPreviousPageProvider provider will be recomputed. But if the value exposed by the provider does not change, then PreviousButton will not rebuild.

This change both improved the performance of our button and had the interesting benefit of extracting the logic outside of our widget.