본문으로 건너뛰기

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:

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:


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.