말랑한 하루

[Flutter] Riverpod의 (Async)NotifierProvider 본문

개발/Flutter

[Flutter] Riverpod의 (Async)NotifierProvider

지수는말랑이 2023. 12. 28. 10:16
반응형

※ 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를 확인할 수 있었습니다. 다음 포스팅에서는 이 모든 것에 대한 내용을 정립하는 시간을 가지도록 하겠습니다.

반응형
Comments