말랑한 하루
[Flutter] Riverpod의 (Async)NotifierProvider 본문
※ reference : https://riverpod.dev/docs/providers/notifier_provider
우리는 지난시간 Riverpod에서 제공하는 Provider에 대해서 알아보았습니다. 하지만, 저번 칼럼에서는 자세한 내용을 다루지 않았습니다. 그 이유는 NotifierProvider와 AsyncNotifierProvider가 가장 대중적으로 사용되기 때문에 실제 개발에 적용할 수 있도록 더 자세한 예시와 함께 설명하고 학습하기 위해서입니다.
이 칼럼에서는 NotifierProvider에 대해서 설명하고, TodoList를 구현하는 내용을 기반으로 사용자 정의(User-defined) Class와 Notifier/AsyncNotifier Class 그리고 각각의 Provider를 구현하고 사용하는 법에 대해서 알려드리겠습니다.
🐇 NotifierProvider
Notifier Class를 수신하고 제공하는데 사용되는 Provider입니다. AsyncNotifierProvider는 AsyncNotifier를 수신하고 제공하는데 사용됩니다.
AsyncNotifier는 비동기적으로 초기화 할 수 있으며, 사용자 상호 작용에 따라 변경될 수 있는 상태관리를 위해 Riverpod가 권장하는 솔루션도 (Async)NotifierProvider, (Async)Notifier과 함께 제공됩니다. 일반적으로 다음과 같이 사용됩니다.
🍒 사용자 정의 이벤트에 반응한 후, 시간이 지남에 따라 변경될 수 있는 상태를 제공합니다.
🍒 비즈니스 로직을 수정하기 위한 모든 과정을 한 곳에서 중앙 집중화 하여, 시간이 지남에 따라 유지 관리 가능성을 향상시킵니다.
🥕 NotifierProvider
🍒 User-defined Class Implement
TodoList에 사용될 Todo를 사용자 지정 클래스로 생성합니다.
여기서 주목해야 할 점은 Todo의 copyWith입니다. copyWith 메소드는 Todo 객체에서 특정 Value를 수정하여 새로운 Todo 객체로 제공하기 위해 만들어논 사용자 정의 함수입니다.
이 사용자 정의 함수에서 사용되는 ?? 문법은 Dart의 문법 중 하나로 if null을 의미합니다.
※ Dart Grammer Document: https://dart-ko.dev/language/operators
class Todo {
const Todo({
required this.id,
required this.description,
required this.completed,
});
// All properties should be `final` on our class.
final String id;
final String description;
final bool completed;
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}
🍒 Notifier Class Implement
Notifier와 NotifierProvider를 구현하는 과정과 설명은 소스코드에 작성되어 있습니다.
class TodosNotifier extends Notifier<List<Todo>> {
// 여기에 TodoList를 초기화하거나, 빈 List로 초기화 합니다.
@override
List<Todo> build() {
return [];
}
void addTodo(Todo todo) {
// notifyListeners 같은 복잡한 과정을 거치지 않습니다.// 우리는 다음과 같이 단순하게 작성하고, 자동으로 재빌드 되도록 구현했습니다.
state = [...state, todo];
}
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)
todo.copyWith(completed: !todo.completed)
else
todo,
];
}
}
// 마지막으로 우리는 사용할 NotifierProvider를 생성합니다.// NotifierProvider는 Notifier class와 함께 UI와 상호작용 합니다.
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(() {
return TodosNotifier();
});
🍒 Useage NotifierProvider on UI
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// list가 체인지 될 때, 다시 빌드될 수 있도록 감시합니다.
List<Todo> todos = ref.watch(todosProvider);
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// todo의 변경된 상태를 변경합니다.
onChanged: (value) =>
ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
여기까지 TodoList를 구현하기 위해 User-defined Class, Notifier Class, NotifierProvider 그리고 사용법까지 알아봤습니다. 다음으로는 같은 소스 코드 내용으로 비동기 처리를 진행하는 AsyncNotifierProvider는 어떻게 진행되는지 보여드리겠습니다.
🥕 AsyncNotifierProvider, AsyncNotifier
🍒 User-defined Class Implement
Notifier Class와 다른점은 copyWith method가 사라지고 fromJson와 toJson 함수가 추가되었다는 점입니다. api 통신 과정에서 JSON 객체를 활용하는 경우가 많기 때문에, 다음의 내용을 잘 숙지하는 것이 좋습니다.
class Todo {
final String id;
final String description;
final bool completed;
const Todo({
required this.id,
required this.description,
required this.completed,
});
factory Todo.fromJson(Map<String, dynamic> map) {
return Todo(
id: map['id'] as String,
description: map['description'] as String,
completed: map['completed'] as bool,
);
}
Map<String, dynamic> toJson() => <String, dynamic>{
'id': id,
'description': description,
'completed': completed,
};
}
🍒 AsyncNotifier Class Implement
async/await를 사용하여 기본 fetch 함수를 생성하는 것이 핵심입니다. 그 과정에서 JSON 객체를 우리가 사용하는 객체로 변환하는 과정을 잊어버리면 안됩니다.
class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
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
Future<List<Todo>> build() async {
// 원격 저장소에서, todo list에 초기화 할 값을 가져옵니다.
return _fetchTodo();
}
Future<void> addTodo(Todo todo) async {
// state의 상태를 loading으로 변경합니다.
state = const AsyncValue.loading();
// 새로운 todo를 추가하고, 새로운 todo list를 원격 저장소에 저장합니다.
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를 선언합니다.
final asyncTodosProvider =
AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(() {
return AsyncTodosNotifier();
});
🍒 Useage AsyncNotifierProvider on UI
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncTodos = ref.watch(asyncTodosProvider);
return switch (asyncTodos) {
AsyncData(:final value) => ListView(
children: [
for (final todo in value)
CheckboxListTile(
value: todo.completed,
onChanged: (value) {
ref.read(asyncTodosProvider.notifier).toggle(todo.id);
},
title: Text(todo.description),
),
],
),
AsyncError(:final error) => Text('Error: $error'),
_ => const Center(child: CircularProgressIndicator()),
};
}
}
우리는 이 과정에서 AsyncValue와 그 값에서 사용할 수 있는 loading/guard method 그리고 AsyncData, AsyncError Class를 확인할 수 있었습니다. 다음 포스팅에서는 이 모든 것에 대한 내용을 정립하는 시간을 가지도록 하겠습니다.
'개발 > Flutter' 카테고리의 다른 글
[Flutter] GoRouter 시작하기 (1) | 2023.12.29 |
---|---|
[Flutter] AsyncValue? (0) | 2023.12.28 |
[Flutter] Riverpod 그리고 Provider (0) | 2023.12.27 |
[Flutter] (Error) Undefined name 'riverpod' used as an annotation. (0) | 2023.12.26 |
[Flutter] Riverpod 시작하기 (0) | 2023.12.26 |