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:
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.