여러 Provider 중 가장 기본적인 것으로 값(value)를 생성하는데 사용됩니다.
주로 아래와 같은 용도로 사용됩니다.
class Todo {
Todo(this.description, this.isCompleted);
final bool isCompleted;
final String description;
}
@riverpod
class Todos extends _$Todos {
@override
List<Todo> build() {
return [];
}
void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO add other methods, such as "removeTodo", ...
}
위와 같은 NotifierProvider 가 있을 때, Provider 를 사용하여 완료된 할일 목록만 노출할 수 있습니다.
@riverpod
List<Todo> completedTodos(CompletedTodosRef ref) {
final todos = ref.watch(todosProvider);
// 완료된 할 일 목록만 반환합니다.
return todos.where((todo) => todo.isCompleted).toList();
}
이제 completedTodosProvider 를 watch 하면 Todos 에 할일이 추가/제거/업데이트 되지 않는 한 완료된 할일 목록은 여러번 읽혀지더라도 다시 계산되지 않습니다. 필터링된 리스트가 캐싱되었다는 겁니다.
Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
});
provider 가 재계산(ref.watch 를 사용하는경우) 되더라도 값이 변경되지 않으면 해당 provider 를 listen 하고 있는 다른 위젯이나 provider 를 업데이트 하지 않습니다.
@riverpod
class PageIndex extends _$PageIndex {
@override
int build() {
return 0;
}
void goToPreviousPage() {
state = state - 1;
}
}
class PreviousButton extends ConsumerWidget {
const PreviousButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 첫 페이지가 아닌 경우, 이전 버튼이 활성화됩니다.
final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;
void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).goToPreviousPage();
}
return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}
이코드의 문제점은 pageIndexProvider 가 변경될때 마다 PreviousButton 이 리빌드 된다는 것 입니다. 이상적인 경우는 버튼이 활성화/비활성화 로 변경될 때만 재빌드하는것 입니다.
문제의 근본 원인은 유저가 이전 페이지로 이동할수 있는지 여부를 previousButton 안에서 직접 계산하고 있기 때문입니다. 이걸 위젯 밖으로 빼서 provider 로 만들면 아래와 같이 됩니다.
@riverpod
class PageIndex extends _$PageIndex {
@override
int build() {
return 0;
}
void goToPreviousPage() {
state = state - 1;
}
}
// 사용자가 이전 페이지로 이동할 수 있는지 여부를 계산하는 프로바이더
@riverpod
bool canGoToPreviousPage(CanGoToPreviousPageRef ref) {
return ref.watch(pageIndexProvider) != 0;
}
class PreviousButton extends ConsumerWidget {
const PreviousButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 이제 새로운 프로바이더를 청취합니다.
// 위젯은 이제 이전 페이지로 이동할 수 있는지 여부를 계산하지 않습니다.
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);
void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).goToPreviousPage();
}
return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}
이제 pageIndex 가 변경될 때 canGoToPreviousPageProvider 가 재계산 되지만 해당 provider 가 노출하는 값이 변경되지 않으면 PriviousButton 은 재빌드되지 않습니다.
이벤트에 반응해서 변경 가능한 상태를 노출하는데 사용됩니다.
상태를 수정하는 코드를 한곳에 모아서 유지보수가 용이하도록 합니다.
예시로 NotifierProvider 를 사용해서 할 일 목록을 구현한걸 보겠습니다.
@freezed
class Todo with _$Todo {
factory Todo({
required String id,
required String description,
required bool completed,
}) = _Todo;
}
@riverpod
class Todos extends _$Todos {
@override
List<Todo> build() {
return [];
}
// UI가 할 일을 추가할 수 있도록 허용합니다.
void addTodo(Todo todo) {
// 상태가 불변이므로 `state.add(todo)`와 같이 할 수 없습니다.
// 대신 이전 항목과 새 항목을 포함하는 새로운 할 일 목록을 생성해야 합니다.
// 여기서 Dart의 전개 연산자를 사용하는 것이 도움이 됩니다!
state = [...state, todo];
// "notifyListeners" 또는 유사한 메서드를 호출할 필요가 없습니다. "state ="을 호출하면 필요할 때 자동으로 UI를 다시 빌드합니다.
}
// 할 일을 삭제하는 것을 허용합니다.
void removeTodo(String todoId) {
// 다시 말하지만, 상태는 불변이므로 기존 목록을 변경하는 대신 새 목록을 만듭니다.
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}
// 할 일을 완료로 표시하는 것을 허용합니다.
void toggle(String todoId) {
state = [
for (final todo in state)
// 일치하는 할 일만 완료로 표시합니다.
if (todo.id == todoId)
// 한 번 더 말하지만, 상태가 불변이므로 할 일의 사본을 만들어야 합니다.
// 이전에 구현한 "copyWith" 메서드를 활용하여 돕습니다.
todo.copyWith(completed: !todo.completed)
else
// 다른 할 일은 수정되지 않습니다.
todo,
];
}
}
위에 정의된 NotifierProvider 를 사용하는 코드를 보겠습니다. (todosProvider 로 접근 가능합니다)
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 할 일 목록이 변경될 때 위젯을 다시 빌드합니다.
List<Todo> todos = ref.watch(todosProvider);
// 할 일을 스크롤 가능한 리스트 뷰에 렌더링합니다.
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// 할 일을 탭하면 완료 상태가 변경됩니다.
onChanged: (value) =>
ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
서버와의 통신하여 비동기로 동작하는 AsyncNotifierProvider 입니다.
@freezed
class Todo with _$Todo {
factory Todo({
required String id,
required String description,
required bool completed,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
@riverpod
class AsyncTodos extends _$AsyncTodos {
Future<List<Todo>> _fetchTodo() async {
final json = await http.get('api/todos');
final todos = jsonDecode(json) as List<Map<String, dynamic>>;
return todos.map(Todo.fromJson).toList();
}
@override
FutureOr<List<Todo>> build() async {
// 원격 저장소에서 초기 할 일 목록을 로드합니다.
return _fetchTodo();
}
Future<void> addTodo(Todo todo) async {
// 상태를 로딩 상태로 설정합니다.
state = const AsyncValue.loading();
// 새 할 일을 추가하고 원격 저장소에서 할 일 목록을 다시 로드합니다.
state = await AsyncValue.guard(() async {
await http.post('api/todos', todo.toJson());
return _fetchTodo();
});
}
// 할 일을 삭제하는 것을 허용합니다.
Future<void> removeTodo(String todoId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await http.delete('api/todos/$todoId');
return _fetchTodo();
});
}
// 할 일을 완료로 표시하는 것을 허용합니다.
Future<void> toggle(String todoId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await http.patch(
'api/todos/$todoId',
<String, dynamic>{'completed': true},
);
return _fetchTodo();
});
}
}
해당 AsyncNotifierProvider 를 사용하는 예시입니다.
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 할 일 목록이 변경될 때 위젯을 다시 빌드합니다.
final asyncTodos = ref.watch(asyncTodosProvider);
// 할 일을 스크롤 가능한 리스트 뷰에 렌더링합니다.
return asyncTodos.when(
data: (todos) => ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// 할 일을 탭하면 완료 상태가 변경됩니다.
onChanged: (value) =>
ref.read(asyncTodosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
),
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (err, stack) => Text('Error: $err'),
);
}
}
provider 와 똑같으나 비동기 작업에 사용된다.
@riverpod
Future<Configuration> fetchConfiguration(FetchConfigurationRef ref) async {
final content = json.decode(
await rootBundle.loadString('assets/configurations.json'),
) as Map<String, Object?>;
return Configuration.fromJson(content);
}
이걸 사용하는 부분은 이렇습니다.
Widget build(BuildContext context, WidgetRef ref) {
final config = ref.watch(fetchConfigurationProvider);
return config.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (config) {
return Text(config.host);
},
);
}
FutureProvider 를 listen 할 경우 위와 같이 AsyncValue를 리턴하게 됩니다.