NotifierProvider란?
NotifierProvider는 Riverpod 2.0에서 도입된 최신 상태 관리 방식으로, StateNotifierProvider의 개선된 버전입니다. 더 간단한 API, 향상된 성능, 그리고 더 나은 개발 경험을 제공합니다. 기존의 StateNotifier보다 적은 보일러플레이트 코드로 동일한 기능을 구현할 수 있으며, 자동 dispose 기능과 향상된 타입 안전성을 제공합니다.
언제 사용하는가?
사용하기 좋은 경우:
• 새로운 프로젝트: Riverpod 2.0+ 환경에서 상태 관리
• 복잡한 상태: 여러 필드를 가진 객체나 비즈니스 로직이 포함된 상태
• 성능이 중요: 최적화된 리빌드와 메모리 관리가 필요한 앱
• 현대적 개발 경험: 최신 Dart 기능과 도구 활용
사용하면 안 되는 경우:
• Riverpod 1.x 환경: 버전 제약으로 사용 불가
• 단순한 상태: 원시 타입 하나만 관리 → StateProvider 사용
• 일회성 비동기: 단순 API 호출 → FutureProvider 사용
• 실시간 스트림: 지속적 데이터 변화 → StreamProvider 사용
• 새로운 프로젝트: Riverpod 2.0+ 환경에서 상태 관리
• 복잡한 상태: 여러 필드를 가진 객체나 비즈니스 로직이 포함된 상태
• 성능이 중요: 최적화된 리빌드와 메모리 관리가 필요한 앱
• 현대적 개발 경험: 최신 Dart 기능과 도구 활용
사용하면 안 되는 경우:
• Riverpod 1.x 환경: 버전 제약으로 사용 불가
• 단순한 상태: 원시 타입 하나만 관리 → StateProvider 사용
• 일회성 비동기: 단순 API 호출 → FutureProvider 사용
• 실시간 스트림: 지속적 데이터 변화 → StreamProvider 사용
기본 사용법
Dart
// 1. 상태 클래스 정의
class Counter {
final int value;
final String status;
const Counter({
required this.value,
required this.status,
});
Counter copyWith({
int? value,
String? status,
}) {
return Counter(
value: value ?? this.value,
status: status ?? this.status,
);
}
}
// 2. Notifier 클래스 정의
class CounterNotifier extends Notifier<Counter> {
@override
Counter build() {
// 초기 상태 반환
return const Counter(value: 0, status: 'idle');
}
void increment() {
state = state.copyWith(
value: state.value + 1,
status: 'incremented',
);
}
void decrement() {
state = state.copyWith(
value: state.value - 1,
status: 'decremented',
);
}
void reset() {
state = const Counter(value: 0, status: 'reset');
}
}
// 3. Provider 정의 - 타입 추론으로 더 간단함
final counterProvider = NotifierProvider<CounterNotifier, Counter>(CounterNotifier.new);
실무 예제: 할 일 목록 관리
Dart
class Todo {
final String id;
final String title;
final bool isCompleted;
final DateTime createdAt;
const Todo({
required this.id,
required this.title,
required this.isCompleted,
required this.createdAt,
});
Todo copyWith({
String? id,
String? title,
bool? isCompleted,
DateTime? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
);
}
}
class TodoList {
final List<Todo> todos;
final TodoFilter filter;
const TodoList({
required this.todos,
required this.filter,
});
TodoList copyWith({
List<Todo>? todos,
TodoFilter? filter,
}) {
return TodoList(
todos: todos ?? this.todos,
filter: filter ?? this.filter,
);
}
List<Todo> get filteredTodos {
switch (filter) {
case TodoFilter.all:
return todos;
case TodoFilter.active:
return todos.where((todo) => !todo.isCompleted).toList();
case TodoFilter.completed:
return todos.where((todo) => todo.isCompleted).toList();
}
}
int get activeCount => todos.where((todo) => !todo.isCompleted).length;
int get completedCount => todos.where((todo) => todo.isCompleted).length;
}
enum TodoFilter { all, active, completed }
class TodoListNotifier extends Notifier<TodoList> {
@override
TodoList build() {
return const TodoList(
todos: [],
filter: TodoFilter.all,
);
}
void addTodo(String title) {
if (title.trim().isEmpty) return;
final newTodo = Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title.trim(),
isCompleted: false,
createdAt: DateTime.now(),
);
state = state.copyWith(
todos: [...state.todos, newTodo],
);
}
void toggleTodo(String id) {
final updatedTodos = state.todos.map((todo) {
if (todo.id == id) {
return todo.copyWith(isCompleted: !todo.isCompleted);
}
return todo;
}).toList();
state = state.copyWith(todos: updatedTodos);
}
void deleteTodo(String id) {
final updatedTodos = state.todos.where((todo) => todo.id != id).toList();
state = state.copyWith(todos: updatedTodos);
}
void updateFilter(TodoFilter filter) {
state = state.copyWith(filter: filter);
}
void clearCompleted() {
final activeTodos = state.todos.where((todo) => !todo.isCompleted).toList();
state = state.copyWith(todos: activeTodos);
}
}
final todoListProvider = NotifierProvider<TodoListNotifier, TodoList>(TodoListNotifier.new);
주의사항과 팁
자주 하는 실수들:
1. build() 메소드에서 비동기 작업:
// ❌ 잘못된 방법
class BadNotifier extends Notifier {
@override
String build() {
fetchData(); // 비동기 작업은 build()에서 하면 안 됨
return 'initial';
}
}
// ✅ 올바른 방법
class GoodNotifier extends Notifier {
@override
String build() {
// build()는 동기적이어야 함
Future.microtask(() => fetchData()); // 다음 틱에서 실행
return 'initial';
}
Future fetchData() async {
// 비동기 작업은 별도 메소드에서
}
}
2. ref 사용 시점 혼동:
// ❌ 잘못된 방법
class BadNotifier extends Notifier {
@override
int build() {
final otherValue = ref.watch(otherProvider); // build()에서는 OK
return 0;
}
void someMethod() {
final value = ref.watch(otherProvider); // 메소드에서는 read() 사용
}
}
// ✅ 올바른 방법
class GoodNotifier extends Notifier {
@override
int build() {
final otherValue = ref.watch(otherProvider); // build()에서는 watch
return 0;
}
void someMethod() {
final value = ref.read(otherProvider); // 메소드에서는 read
}
}
베스트 프랙티스:
• build() 메소드는 순수하게 유지
• ref.watch는 build()에서만, ref.read는 메소드에서
• 복잡한 로직은 private 메소드로 분리
• 에러 상태도 상태 클래스에 포함
• 테스트하기 쉬운 구조로 설계
1. build() 메소드에서 비동기 작업:
`dart// ❌ 잘못된 방법
class BadNotifier extends Notifier
@override
String build() {
fetchData(); // 비동기 작업은 build()에서 하면 안 됨
return 'initial';
}
}
// ✅ 올바른 방법
class GoodNotifier extends Notifier
@override
String build() {
// build()는 동기적이어야 함
Future.microtask(() => fetchData()); // 다음 틱에서 실행
return 'initial';
}
Future
// 비동기 작업은 별도 메소드에서
}
}
`2. ref 사용 시점 혼동:
`dart// ❌ 잘못된 방법
class BadNotifier extends Notifier
@override
int build() {
final otherValue = ref.watch(otherProvider); // build()에서는 OK
return 0;
}
void someMethod() {
final value = ref.watch(otherProvider); // 메소드에서는 read() 사용
}
}
// ✅ 올바른 방법
class GoodNotifier extends Notifier
@override
int build() {
final otherValue = ref.watch(otherProvider); // build()에서는 watch
return 0;
}
void someMethod() {
final value = ref.read(otherProvider); // 메소드에서는 read
}
}
`베스트 프랙티스:
• build() 메소드는 순수하게 유지
• ref.watch는 build()에서만, ref.read는 메소드에서
• 복잡한 로직은 private 메소드로 분리
• 에러 상태도 상태 클래스에 포함
• 테스트하기 쉬운 구조로 설계
다른 Provider와 비교
NotifierProvider vs StateNotifierProvider:
• NotifierProvider: Riverpod 2.0+, Notifier 상속, build() 메소드, 자동 dispose, 향상된 성능
• StateNotifierProvider: Riverpod 1.0+, StateNotifier 상속, 생성자, 수동 dispose, 기본 성능
NotifierProvider vs ChangeNotifierProvider:
• NotifierProvider: 불변(immutable) 상태, 우수한 성능, 높은 예측 가능성, Riverpod 최적화
• ChangeNotifierProvider: 가변(mutable) 상태, 보통 성능, 낮은 예측 가능성, Flutter 기본
선택 기준:
• 새 프로젝트 + Riverpod 2.0+: NotifierProvider 최우선
• 복잡한 상태 관리: NotifierProvider
• 최고 성능 필요: NotifierProvider
• Riverpod 1.x: StateNotifierProvider
• 기존 코드 유지: ChangeNotifierProvider
• NotifierProvider: Riverpod 2.0+, Notifier 상속, build() 메소드, 자동 dispose, 향상된 성능
• StateNotifierProvider: Riverpod 1.0+, StateNotifier 상속, 생성자, 수동 dispose, 기본 성능
NotifierProvider vs ChangeNotifierProvider:
• NotifierProvider: 불변(immutable) 상태, 우수한 성능, 높은 예측 가능성, Riverpod 최적화
• ChangeNotifierProvider: 가변(mutable) 상태, 보통 성능, 낮은 예측 가능성, Flutter 기본
선택 기준:
• 새 프로젝트 + Riverpod 2.0+: NotifierProvider 최우선
• 복잡한 상태 관리: NotifierProvider
• 최고 성능 필요: NotifierProvider
• Riverpod 1.x: StateNotifierProvider
• 기존 코드 유지: ChangeNotifierProvider
정리
NotifierProvider는 Riverpod 2.0의 플래그십 상태 관리 솔루션입니다. 기존 StateNotifierProvider의 모든 장점을 유지하면서 더 간단하고 성능이 좋은 API를 제공합니다.
핵심 포인트:
• 최신 Riverpod: 2.0+에서만 사용 가능한 차세대 솔루션
• 향상된 성능: 자동 최적화와 효율적인 리빌드
• 간단한 API: 적은 보일러플레이트와 직관적인 구조
• 완전한 타입 안전성: 컴파일 타임 에러 방지
다음 편 예고: 8편에서는 비동기 작업과 상태 관리를 완벽하게 결합하는 AsyncNotifierProvider에 대해 알아보겠습니다.
핵심 포인트:
• 최신 Riverpod: 2.0+에서만 사용 가능한 차세대 솔루션
• 향상된 성능: 자동 최적화와 효율적인 리빌드
• 간단한 API: 적은 보일러플레이트와 직관적인 구조
• 완전한 타입 안전성: 컴파일 타임 에러 방지
다음 편 예고: 8편에서는 비동기 작업과 상태 관리를 완벽하게 결합하는 AsyncNotifierProvider에 대해 알아보겠습니다.