`StateNotifier`에서
Riverpod 2.0과 함께 새로운 클래스가 도입되었습니다: Notifier / AsyncNotifier.
이제 이러한 새로운 API를 위해 StateNotifier는 더 이상 사용되지 않습니다.
이 페이지는 더 이상 사용되지 않는 StateNotifier에서 새로운 API로 마이그레이션하는 방법을 보여줍니다.
AsyncNotifier가 도입한 주요 이점은 더 나은 async 지원입니다,
실제로 AsyncNotifier는 UI에서 수정할 수 있는 방법을 노출하는 FutureProvider로 생각할 수 있습니다.
또한, 새로운 (Async)Notifier가 추가되었습니다:
- 클래스 내부에 
Ref객체 노출하기 - 코드 생성 방식(codegen)과 비코드 생성 방식(non-codegen) 간에 유사한 문법 제공
 - 동기화 버전과 비동기 버전 간에 유사한 문법 제공
 - 로직을 provider에서 벗어나 Notifiers 자체로 중앙 집중화하기
 
Notifier를 정의하는 방법, StateNotifier와 비교하는 방법, 비동기 상태를 위해 새로운 AsyncNotifier를 마이그레이션하는 방법을 살펴봅시다.
새로운 문법 비교
이 비교를 시작하기 전에 Notifier을 정의하는 방법을 알아두세요.
이전 StateNotifier 문법을 사용하여 예제를 작성해 보겠습니다:
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
  void decrement() => state--;
}
final counterNotifierProvider =
    StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});
다음은 새로운 Notifier API로 작성된 동일한 예시이며, 대략 다음과 같이 변환됩니다:
- riverpod
 - riverpod_generator
 
class CounterNotifier extends Notifier<int> {
  
  int build() => 0;
  void increment() => state++;
  void decrement() => state++;
}
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends _$CounterNotifier {
  
  int build() => 0;
  void increment() => state++;
  void decrement() => state--;
}
Notifier와 StateNotifier를 비교하면 다음과 같은 주요 차이점을 확인할 수 있습니다:
StateNotifier의 반응형 종속성(reactive dependencies)은 provider에서 선언되는 반면,Notifier는 이 로직을build메서드에서 중앙 집중화합니다.StateNotifier의 전체 초기화 프로세스는 provider와 생성자 사이에 분할되어 있는 반면,Notifier는 이러한 로직을 배치할 수 있는 단일 위치를 예약합니다.StateNotifier와는 반대로,Notifier의 생성자에는 어떠한 로직도 작성되지 않는 것을 주목하세요.
Notifier의 비동기 대응 클래스인 AsyncNotifier에서도 비슷한 점을 발견할 수 있습니다.
비동기 StateNotifier 마이그레이션하기
새로운 API 구문의 가장 큰 장점은 비동기 데이터에 대한 향상된 DX입니다.
다음 예시를 살펴보겠습니다:
class AsyncTodosNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
  AsyncTodosNotifier() : super(const AsyncLoading()) {
    _postInit();
  }
  Future<void> _postInit() async {
    state = await AsyncValue.guard(() async {
      final json = await http.get('api/todos');
      return [...json.map(Todo.fromJson)];
    });
  }
  // ...
}
다음은 새로운 AsyncNotifier API를 사용하여 재작성된 위의 예시입니다:
- riverpod
 - riverpod_generator
 
class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
  
  FutureOr<List<Todo>> build() async {
    final json = await http.get('api/todos');
    return [...json.map(Todo.fromJson)];
  }
  // ...
}
final asyncTodosNotifier =
    AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(
  AsyncTodosNotifier.new,
);
class AsyncTodosNotifier extends _$AsyncTodosNotifier {
  
  FutureOr<List<Todo>> build() async {
    final json = await http.get('api/todos');
    return [...json.map(Todo.fromJson)];
  }
  // ...
}
AsyncNotifier는 Notifier와 마찬가지로 더 간단하고 통일된 API를 제공합니다.
여기서 AsyncNotifier는 메서드가 있는 FutureProvider로 쉽게 볼 수 있습니다.
AsyncNotifier에는 StateNotifier에는 없는 유틸리티와 게터가 함께 제공됩니다,
예를 들어 future
및 update.
이를 통해 비동기 변이(mutations)와 부수작업(side-effects)을 처리할 때 훨씬 더 간단한 로직을 작성할 수 있습니다.
StateNotifier<AsyncValue<T>>에서 AsyncNotifier<T>로 마이그레이션하는 방법은 다음과 같습니다:
- 초기화 로직을 
build에 넣기 - 초기화 또는 부수작업 메서드에서 
catch/try블록을 제거합니다. build에서AsyncValue.guard를 제거합니다.Future를AsyncValue로 변환하기 때문입니다.
장점
이 몇 가지 예시를 살펴본 후, 이제 Notifier 와 AsyncNotifier의 주요 장점을 살펴보겠습니다:
- 새로운 구문은 특히 비동기 상태의 경우 훨씬 더 간단하고 가독성이 높아질 것입니다.
 - 새로운 API에는 일반적으로 상용구 코드가 줄어들 가능성이 높습니다.
 - 이제 작성하는 provider 타입에 관계없이 구문이 통합되어 코드 생성이 가능해졌습니다. (코드 생성(Code generation)에 대한 정보 참조).
 
더 자세히 살펴보고 더 많은 차이점과 유사점을 강조해 보겠습니다.
명시적인 .family 및 .autoDispose 수정사항
또 다른 중요한 차이점은 새로운 API로 패밀리 및 자동폐기가 처리되는 방식입니다.
Notifier에는 FamilyNotifier 및 Notifier와 같은 자체 .family 및 .autoDispose 대응 항목이 있습니다.
항상 그렇듯이, 이러한 수정 사항을 결합할 수 있습니다 (일명 AutoDisposeFamilyNotifier).
AsyncNotifier에는 비동기 버전도 있습니다(예: FamilyAsyncNotifier).
수정 사항(Modifications)은 클래스 내부에 명시적으로 지정(stated)됩니다;
모든 매개변수는 build 메서드에 직접 주입되어 초기화 로직에서 사용할 수 있습니다.
이렇게 하면 가독성이 향상되고 간결해지며 전반적으로 실수가 줄어듭니다.
다음 예제에서는 StateNotifierProvider.family를 정의하고 있습니다.
class BugsEncounteredNotifier extends StateNotifier<AsyncValue<int>> {
  BugsEncounteredNotifier({
    required this.ref,
    required this.featureId,
  }) : super(const AsyncData(99));
  final String featureId;
  final Ref ref;
  Future<void> fix(int amount) async {
    state = await AsyncValue.guard(() async {
      final old = state.requireValue;
      final result =
          await ref.read(taskTrackerProvider).fix(id: featureId, fixed: amount);
      return max(old - result, 0);
    });
  }
}
final bugsEncounteredNotifierProvider = StateNotifierProvider.family
    .autoDispose<BugsEncounteredNotifier, AsyncValue<int>, String>((ref, id) {
  return BugsEncounteredNotifier(ref: ref, featureId: id);
});
BugsEncounteredNotifier은 무겁거나 읽기 어려운 느낌입니다.
마이그레이션 된 AsyncNotifier를 살펴 보겠습니다:
- riverpod
 - riverpod_generator
 
class BugsEncounteredNotifier extends AsyncNotifier<int> {
  BugsEncounteredNotifier(this.arg);
  final String arg;
  
  FutureOr<int> build() {
    return 99;
  }
  Future<void> fix(int amount) async {
    final old = await future;
    final result =
        await ref.read(taskTrackerProvider).fix(id: this.arg, fixed: amount);
    state = AsyncData(max(old - result, 0));
  }
}
final bugsEncounteredNotifierProvider = AsyncNotifierProvider.family
    .autoDispose<BugsEncounteredNotifier, int, String>(
  BugsEncounteredNotifier.new,
);
class BugsEncounteredNotifier extends _$BugsEncounteredNotifier {
  
  FutureOr<int> build(String featureId) {
    return 99;
  }
  Future<void> fix(int amount) async {
    final old = await future;
    final result = await ref
        .read(taskTrackerProvider)
        .fix(id: this.featureId, fixed: amount);
    state = AsyncData(max(old - result, 0));
  }
}
마이그레이션된 버전은 가볍게 읽을 수 있는 수준입니다.
(Async)Notifier의 .family 매개변수는 this.arg(또는 코드생성을 사용하는 경우 this.paramName)를 통해 사용할 수 있습니다.
라이프사이클에 따라 동작 방식이 다릅니다.
Notifier/AsyncNotifier와 StateNotifier의 수명 주기는 크게 다릅니다.
이 예시는 이전 API의 로직이 얼마나 부족한지(sparse)를 다시 한 번 보여줍니다:
class MyNotifier extends StateNotifier<int> {
  MyNotifier(this.ref, this.period) : super(0) {
    // 1 init logic
    _timer = Timer.periodic(period, (t) => update()); // 2 side effect on init
  }
  final Duration period;
  final Ref ref;
  late final Timer _timer;
  Future<void> update() async {
    await ref.read(repositoryProvider).update(state + 1); // 3 mutation
    if (mounted) state++; // 4 check for mounted props
  }
  
  void dispose() {
    _timer.cancel(); // 5 custom dispose logic
    super.dispose();
  }
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
  // 6 provider definition
  final period = ref.watch(durationProvider); // 7 reactive dependency logic
  return MyNotifier(ref, period); // 8 pipe down `ref`
});
여기서 durationProvider가 업데이트되면 MyNotifier를 페기(dispose): 인스턴스가 다시 인스턴스화되고 내부 상태가 다시 초기화됩니다.
또한 다른 모든 provider와 달리 dispose 콜백은 클래스에서 별도로 정의해야 합니다.
마지막으로, provider에 ref.onDispose를 작성하는 것이 여전히 가능하기 때문에, 이 API의 로직이 얼마나 부족한지(sparse)를 다시 한 번 알 수 있습니다;
잠재적으로 개발자는 이 Notifier 동작을 이해하기 위해 여덟 곳(8개!)을 살펴봐야 할 수도 있습니다!
이러한 모호함은 Riverpod 2.0을 통해 해결되었습니다.
이전의 dispose vs ref.onDispose
StateNotifier의 dispose 메서드는 notifier 자체의 폐기(dispose) 이벤트를 참조하며, 일명 자신을 처분하기 전에(before disposing of itself) 호출되는 콜백입니다.
(Async)Notifier은 이 속성을 갖지 않는데, 리빌드 시 폐기되지 않고 내부 상태만 폐기되기 때문입니다.
새로운 notifiers에서 폐기 수명주기는 다른 provider와 마찬가지로 ref.onDispose(및 기타)를 통해 한 곳에서만 처리됩니다.
이렇게 하면 API와 DX가 단순화되어 라이프사이클 부작용을 이해하기 위해 살펴봐야 할 곳이 build 메서드 하나만 남게 됩니다.
간단히 말해서, 내부 상태(internal state)가 다시 빌드되기 전에 실행되는 콜백을 등록하려면 다른 모든 provider와 마찬가지로 ref.onDispose를 사용하면 됩니다.
위의 스니펫을 다음과 같이 마이그레이션할 수 있습니다:
- riverpod
 - riverpod_generator
 
class MyNotifier extends Notifier<int> {
  
  int build() {
    // 코드를 한 곳에서 읽고 쓰기만 하면 됩니다.
    final period = ref.watch(durationProvider);
    final timer = Timer.periodic(period, (t) => update());
    ref.onDispose(timer.cancel);
    return 0;
  }
  Future<void> update() async {
    await ref.read(repositoryProvider).update(state + 1);
    // `mounted`는 더 이상 없습니다!
    state++; // throw될 수도 있습니다.
  }
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
class MyNotifier extends _$MyNotifier {
  
  int build() {
    // 코드를 한 곳에서 읽고 쓰기만 하면 됩니다.
    final period = ref.watch(durationProvider);
    final timer = Timer.periodic(period, (t) => update());
    ref.onDispose(timer.cancel);
    return 0;
  }
  Future<void> update() async {
    await ref.read(repositoryProvider).update(state + 1);
    // `mounted`는 더 이상 없습니다!
    state++; // throw될 수도 있습니다.
  }
}
이 마지막 스니펫에는 확실히 약간의 단순화가 있지만 여전히 열려 있는 문제가 있습니다:
이제 update를 수행하는 동안 notifiers가 아직 살아(alive)있는지 여부를 파악할 수 없습니다.
이로 인해 원치 않는 StateError가 발생할 수 있습니다.
더 이상 마운트되지(mounted) 않음
이는 (Async)Notifier에 StateNotifier에서 사용할 수 있는 mounted 프로퍼티가 없기 때문에 발생합니다.
수명 주기의 차이를 고려하면 이것은 완벽하게 이해가 됩니다;
가능하긴 하지만, 새로운 notifiers에서 mounted 프로퍼티는 오해의 소지가 있습니다: mounted는 거의 항상 true이 될 것입니다.
커스텀 해결방법을 만들 수는 있지만, 비동기 작업을 취소하여 이 문제를 해결하는 것이 좋습니다.
작업 취소는 커스텀 Completer 또는 커스텀 파생어(derivative)를 사용하여 수행할 수 있습니다.
예를 들어 Dio를 사용하여 네트워크 요청을 수행하는 경우 cancel token을 사용하는 것이 좋습니다.
(Automatic disposal 참고)
따라서 위의 예는 다음과 같이 마이그레이션됩니다:
- riverpod
 - riverpod_generator
 
class MyNotifier extends Notifier<int> {
  
  int build() {
    // 코드를 한 곳에서 읽고 쓰기만 하면 됩니다.
    final period = ref.watch(durationProvider);
    final timer = Timer.periodic(period, (t) => update());
    ref.onDispose(timer.cancel);
    return 0;
  }
  Future<void> update() async {
    final cancelToken = CancelToken();
    ref.onDispose(cancelToken.cancel);
    await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
    state++;
  }
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
class MyNotifier extends _$MyNotifier {
  
  int build() {
    // 코드를 한 곳에서 읽고 쓰기만 하면 됩니다.
    final period = ref.watch(durationProvider);
    final timer = Timer.periodic(period, (t) => update());
    ref.onDispose(timer.cancel);
    return 0;
  }
  Future<void> update() async {
    final cancelToken = CancelToken();
    ref.onDispose(cancelToken.cancel);
    await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
    // 취소 토큰이 호출되면 사용자 정의 예외가 발생합니다.
    state++;
  }
}
변이(Mutations) API는 이전과 동일합니다
지금까지 StateNotifier와 새로운 API의 차이점을 살펴보았습니다.
대신, Notifier, AsyncNotifier, StateNotifier가 공유하는 한 가지는 상태를 소비하고 변경할 수 있다는 점입니다.
Consumers는 동일한 구문으로 이 세 providers로부터 데이터를 얻을 수 있습니다,
이는 StateNotifier에서 마이그레이션하는 경우에 유용하며, 이는 notifiers 메서드에도 적용됩니다.
class SomeConsumer extends ConsumerWidget {
  const SomeConsumer({super.key});
  
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterNotifierProvider);
    return Column(
      children: [
        Text("You've counted up until $counter, good job!"),
        TextButton(
          onPressed: () =>
              ref.read(counterNotifierProvider.notifier).increment(),
          child: const Text('Count even more!'),
        ),
      ],
    );
  }
}
기타 마이그레이션
StateNotifier와 Notifier(또는 AsyncNotifier)의 영향력이 크지 않은 차이점을 살펴봅시다.
.addListener 및 .stream에서
StateNotifier의 .addListener와 .stream은 상태 변경을 수신하는 데 사용할 수 있습니다.
이 두 API는 이제 오래된 것으로 간주됩니다.
이는 Notifier, AsyncNotifier 및 기타 proviers와 완전한 API 통일성을 달성하기 위한 의도적인 것입니다.
실제로 Notifier나 AsyncNotifier를 사용하는 것은 다른 provier와 다르지 않아야 합니다.
따라서 이 것이:
class MyNotifier extends StateNotifier<int> {
  MyNotifier() : super(0);
  void add() => state++;
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
  final notifier = MyNotifier();
  final cleanup = notifier.addListener((state) => debugPrint('$state'));
  ref.onDispose(cleanup);
  // 또는 동등하게:
  // final listener = notifier.stream.listen((event) => debugPrint('$event'));
  // ref.onDispose(listener.cancel);
  return notifier;
});
이렇게 됩니다:
- riverpod
 - riverpod_generator
 
class MyNotifier extends Notifier<int> {
  
  int build() {
    listenSelf((_, next) => debugPrint('$next'));
    return 0;
  }
  void add() => state++;
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
class MyNotifier extends _$MyNotifier {
  
  int build() {
    listenSelf((_, next) => debugPrint('$next'));
    return 0;
  }
  void add() => state++;
}
간단히 말해, Notifier/AsyncNotifier를 수신하려면 ref.listen를 사용하면 됩니다.
테스트의 .debugState에서
StateNotifier는 .debugState를 노출합니다:
이 프로퍼티는 개발 모드에서 테스트 목적으로 클래스 외부에서 상태 액세스를 활성화하기 위해 pkg:state_notifier 사용자가 사용할 수 있습니다.
테스트에서 상태에 액세스하기 위해 .debugState를 사용하는 경우 이 접근 방식을 중단해야 합니다.
Notifier / AsyncNotifier에는 .debugState가 없으며, 대신 .state, 즉 @visibleForTesting을 직접 노출합니다.
AVOID 테스트에서 .state에 접근하지 마시고, 꼭 접근해야 한다면 Notifier / AsyncNotifier가 이미 제대로 인스턴스화된 경우에만 접근하세요;
그러면 테스트 내부에서 .state에 자유롭게 접근할 수 있습니다.
실제로 Notifier / AsyncNotifier는 직접 인스턴스화해서는 안 됩니다;
대신 해당 provider를 사용해 상호작용해야 합니다:
그렇게 하지 않으면 notifier가 중단(break)됩니다,
ref와 family 인자가 초기화되지 않기 때문입니다.
Notifier 인스턴스가 없으신가요?
문제없습니다. 노출된 상태를 읽을 때와 마찬가지로 ref.read로 인스턴스를 가져올 수 있습니다:
void main(List<String> args) {
  test('my test', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);
    // notifier 가져오기
    final Notifier<int> notifier = container.read(myNotifierProvider.notifier);
    // 노출된 상태 가져오기
    final int state = container.read(myNotifierProvider);
    // TODO 테스트 작성하기
  });
}
전용 가이드에서 테스트에 대해 자세히 알아보세요. providers 테스트하기를 참고하세요.
StateProvider에서
StateProvider는 Riverpod에서 출시 이후 노출된 것으로, StateNotifierProvider의 간소화된 버전을 위해 몇 가지 LoC를 절약하기 위해 만들어졌습니다.
StateNotifierProvider는 더 이상 사용되지 않으므로 StateProvider도 피해야 합니다.
또한 현재는 새로운 API에 상응하는 StateProvider가 없습니다.
그럼에도 불구하고 StateProvider에서 Notifier로 마이그레이션하는 것은 간단합니다.
이 코드는:
final counterProvider = StateProvider<int>((ref) {
  return 0;
});
이렇게 됩니다:
- riverpod
 - riverpod_generator
 
class CounterNotifier extends Notifier<int> {
  
  int build() => 0;
  
  set state(int newState) => super.state = newState;
  int update(int Function(int state) cb) => state = cb(state);
}
final counterNotifierProvider =
    NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends _$CounterNotifier {
  
  int build() => 0;
  
  set state(int newState) => super.state = newState;
  int update(int Function(int state) cb) => state = cb(state);
}
LoC가 몇 개 더 들더라도 StateProvider에서 마이그레이션하면 StateNotifier를 확실하게 보존(archive)할 수 있습니다.