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 "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) {
// todosProvider로부터 모든 할일(todos)목록을 가져옵니다.
final todos = ref.watch(todosProvider);
// 완료된(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 a ListView/GridView/...등을 사용하여 todos를 표시하기/* 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:
final pageIndexProvider = StateProvider<int>((ref) => 0);
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).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);
// 사용자(the user)가 이전 페이지로 돌아가는지 계산하기 위한 프로바이더
final canGoToPreviousPageProvider = Provider<bool>((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).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.