Skip to main content

Provider

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 StateNotifierProvider which manipulates a list of todos:

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

class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);

void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO add other methods, such as "removeTodo", ...
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});

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

final completedTodosProvider = Provider<List<Todo>>((ref) {
// We obtain the list of all todos from the todosProvider
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/...
});

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 naïve 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:

final pageIndexProvider = StateProvider<int>((ref) => 0);

class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}): super(key: key);

@override
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).update((state) => state - 1);
}

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:

final pageIndexProvider = StateProvider<int>((ref) => 0);

// A provider which computes whether the user is allowed to go to the previous page
final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) != 0;
});

class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}): super(key: key);

@override
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).update((state) => state - 1);
}

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.