본문으로 바로가기
  1. Home
  2. Flutter/Package
  3. Flutter Riverpod All Providers (5-2) - (Async)NotifierProvider

Flutter Riverpod All Providers (5-2) - (Async)NotifierProvider

· 댓글개 · Dev_Whale

NotifierProvider는 Notifier를 수신하고 노출하는 데 사용되는 제공자입니다.

AsyncNotifierProvider는 비동기 Notifier를 수신하고 노출하는 데 사용되는 provider입니다. 비동기적으로 초기화할 수 있는 Notifier입니다.

(Async)NotifierProvider와 함께 Riverpod가 사용자 상호작용에 반응하여 변경될 수 있는 상태를 관리하기 위해 권장하는 솔루션은 (Async)Notifier입니다.

 

일반적으로 다음과 같은 용도로 사용됩니다:

  • 사용자 정의 이벤트에 반응한 후 시간이 지남에 따라 변경될 수 있는 상태를 노출합니다.
  • 일부 상태(일명 '비즈니스 로직')를 수정하는 로직을 한 곳에 집중시켜 시간이 지남에 따라 유지 관리성을 개선합니다.

예시로 NotifierProvider를 사용하여 할 일 목록을 구현할 수 있습니다. 이렇게 하면 addTodo와 같은 메서드를 노출하여 UI가 사용자 상호작용에 따라 할 일 목록을 업데이트할 수 있도록 할 수 있습니다:

// 변하지 않는 상태를 선호합니다.
// 구현을 돕기 위해 Freezed와 같은 패키지를 사용할 수도 있습니다.
@immutable
class Todo {
  const Todo({
    required this.id,
    required this.description,
    required this.completed,
  });

  // 모든 프로퍼티는 클래스에서 'final'이어야 합니다.
  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,
    );
  }
}

// NotifierProvider에 전달할 Notifier 클래스입니다.
// 이 클래스는 "state" 프로퍼티 외부에 상태를 노출해서는 안 되며
// 이는 퍼블릭 getters/property가 없음을 의미합니다!
// 이 클래스의 public 메서드는 UI가 상태를 수정할 수 있게 해줍니다.
class TodosNotifier extends Notifier<List<Todo>> {
  // 할 일 목록을 빈 목록으로 초기화합니다.
  @override
  List<Todo> build() {
    return [];
  }

  // UI가 할 일을 추가할 수 있도록 허용해 봅시다.
  void addTodo(Todo todo) {
    // 상태는 불변이므로 `state.add(todo)`를 수행할 수 없습니다.
    // 대신 이전 항목과 새 항목이 포함된 새 할 일 목록을 만들어야 합니다.
    // 여기서 다트의 스프레드 연산자를 사용하면 도움이 됩니다!
    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
          // other todos are not modified
          todo,
    ];
  }
}

// 마지막으로 NotifierProvider를 사용하여 UI가 TodosNotifier 클래스와 상호 작용할 수 있도록 합니다.
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(() {
  return TodosNotifier();
});

이제 NotifierProvider를 정의했으므로 이를 사용하여 UI의 할 일 목록과 상호작용할 수 있습니다:

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를 사용하여 외부 할 일 목록을 구현할 수 있습니다. 이렇게 하면 addTodo와 같은 메서드를 노출하여 UI가 사용자 상호작용에 따라 할 일 목록을 수정할 수 있습니다:

// 불변 상태를 사용하는 것이 좋습니다.
// 구현을 돕기 위해 Freezed와 같은 패키지를 사용할 수도 있습니다.
@immutable
class Todo {
  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,
    );
  }

  // 모든 프로퍼티는 클래스에서 'final'이어야 합니다.
  final String id;
  final String description;
  final bool completed;

  Map<String, dynamic> toJson() => <String, dynamic>{
        'id': id,
        'description': description,
        'completed': completed,
      };
}

// NotifierProvider에 전달할 Notifier 클래스입니다.
// 이 클래스는 "state" 프로퍼티 외부에 상태를 노출해서는 안 되며
// 이는 퍼블릭 getters/property가 없음을 의미합니다!
// 이 클래스의 public 메서드는 UI가 상태를 수정할 수 있게 해줍니다.
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 {
    // 원격 리포지토리에서 최초의 할 일 목록 불러오기
    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();
    });
  }
}

// 마지막으로 NotifierProvider를 사용하여 UI가 TodosNotifier 클래스와 상호 작용할 수 있도록 합니다.
final asyncTodosProvider =
    AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(() {
  return AsyncTodosNotifier();
});

이제 AsyncNotifierProvider를 정의했으므로 이를 사용하여 UI의 할 일 목록과 상호작용할 수 있습니다:

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'),
    );
  }
}
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.