AsyncNotifierProvider란?
AsyncNotifierProvider는 Riverpod 2.0에서 도입된 비동기 상태 관리 전용 Provider입니다. API 호출, 데이터베이스 작업, 파일 I/O 등의 비동기 작업과 복잡한 상태 관리를 하나로 통합합니다. FutureProvider의 일회성 특성과 StateNotifierProvider의 상태 관리 능력을 결합한 형태로, 로딩, 성공, 에러 상태를 자동으로 관리하면서도 상태 변경 메소드를 제공합니다.
언제 사용하는가?
사용하기 좋은 경우:
• CRUD 작업: 데이터 생성, 읽기, 수정, 삭제가 모두 필요한 경우
• 데이터 캐싱: API 데이터를 가져와서 로컬에서 조작
• 실시간 + 비동기: 초기 로딩 후 실시간 업데이트
• 복잡한 비동기 플로우: 여러 단계의 비동기 작업이 연결된 경우
• 오프라인 지원: 로컬 캐시와 원격 동기화
사용하면 안 되는 경우:
• 단순한 일회성 API 호출: FutureProvider가 더 적합
• 실시간 스트림만 필요: StreamProvider 사용
• 동기적 상태만 관리: NotifierProvider 사용
• Riverpod 1.x 환경: 버전 제약으로 사용 불가
• CRUD 작업: 데이터 생성, 읽기, 수정, 삭제가 모두 필요한 경우
• 데이터 캐싱: API 데이터를 가져와서 로컬에서 조작
• 실시간 + 비동기: 초기 로딩 후 실시간 업데이트
• 복잡한 비동기 플로우: 여러 단계의 비동기 작업이 연결된 경우
• 오프라인 지원: 로컬 캐시와 원격 동기화
사용하면 안 되는 경우:
• 단순한 일회성 API 호출: FutureProvider가 더 적합
• 실시간 스트림만 필요: StreamProvider 사용
• 동기적 상태만 관리: NotifierProvider 사용
• Riverpod 1.x 환경: 버전 제약으로 사용 불가
기본 사용법
Dart
// 1. 데이터 모델 정의
class User {
final String id;
final String name;
final String email;
final DateTime lastSeen;
const User({
required this.id,
required this.name,
required this.email,
required this.lastSeen,
});
User copyWith({
String? id,
String? name,
String? email,
DateTime? lastSeen,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
lastSeen: lastSeen ?? this.lastSeen,
);
}
}
// 2. AsyncNotifier 클래스 정의
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
// 초기 데이터 로딩 - 자동으로 AsyncValue로 래핑됨
return await _fetchUser();
}
Future<User> _fetchUser() async {
// API 호출 시뮬레이션
await Future.delayed(const Duration(seconds: 2));
return User(
id: '1',
name: 'John Doe',
email: 'john@example.com',
lastSeen: DateTime.now(),
);
}
Future<void> updateName(String newName) async {
// 낙관적 업데이트: UI를 먼저 업데이트
final currentUser = state.value;
if (currentUser == null) return;
final updatedUser = currentUser.copyWith(name: newName);
state = AsyncValue.data(updatedUser);
try {
// 서버에 변경사항 전송
await _updateUserOnServer(updatedUser);
} catch (e) {
// 실패 시 원래 상태로 복구
state = AsyncValue.data(currentUser);
throw e;
}
}
Future<void> _updateUserOnServer(User user) async {
await Future.delayed(const Duration(seconds: 1));
// 실제로는 HTTP PUT 요청
}
Future<void> refresh() async {
// 데이터 새로고침
state = const AsyncValue.loading();
try {
final user = await _fetchUser();
state = AsyncValue.data(user);
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
// 3. Provider 정의
final userProvider = AsyncNotifierProvider<UserNotifier, User>(UserNotifier.new);
// 4. UI에서 사용
class UserProfileWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('오류: $error'),
data: (user) => Column(
children: [
Text('이름: ${user.name}'),
Text('이메일: ${user.email}'),
ElevatedButton(
onPressed: () => ref.read(userProvider.notifier).updateName('New Name'),
child: const Text('이름 변경'),
),
],
),
);
}
}
실무 예제: 상품 목록 관리 (CRUD)
Dart
class Product {
final String id;
final String name;
final double price;
final String description;
final bool isAvailable;
const Product({
required this.id,
required this.name,
required this.price,
required this.description,
required this.isAvailable,
});
Product copyWith({
String? id,
String? name,
double? price,
String? description,
bool? isAvailable,
}) {
return Product(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
description: description ?? this.description,
isAvailable: isAvailable ?? this.isAvailable,
);
}
}
class ProductListNotifier extends AsyncNotifier<List<Product>> {
@override
Future<List<Product>> build() async {
// 초기 상품 목록 로딩
return await _fetchProducts();
}
Future<List<Product>> _fetchProducts() async {
await Future.delayed(const Duration(seconds: 1));
// 실제로는 API 호출
return [
const Product(
id: '1',
name: '노트북',
price: 1200000,
description: '고성능 게이밍 노트북',
isAvailable: true,
),
const Product(
id: '2',
name: '마우스',
price: 50000,
description: '무선 게이밍 마우스',
isAvailable: true,
),
];
}
Future<void> addProduct(Product product) async {
final currentProducts = state.value ?? [];
// 낙관적 업데이트
state = AsyncValue.data([...currentProducts, product]);
try {
await _createProductOnServer(product);
} catch (e) {
// 실패 시 원래 상태로 복구
state = AsyncValue.data(currentProducts);
rethrow;
}
}
Future<void> updateProduct(Product updatedProduct) async {
final currentProducts = state.value ?? [];
// 낙관적 업데이트
final updatedList = currentProducts.map((product) {
return product.id == updatedProduct.id ? updatedProduct : product;
}).toList();
state = AsyncValue.data(updatedList);
try {
await _updateProductOnServer(updatedProduct);
} catch (e) {
// 실패 시 원래 상태로 복구
state = AsyncValue.data(currentProducts);
rethrow;
}
}
Future<void> deleteProduct(String productId) async {
final currentProducts = state.value ?? [];
// 낙관적 업데이트
final updatedList = currentProducts.where((p) => p.id != productId).toList();
state = AsyncValue.data(updatedList);
try {
await _deleteProductOnServer(productId);
} catch (e) {
// 실패 시 원래 상태로 복구
state = AsyncValue.data(currentProducts);
rethrow;
}
}
Future<void> toggleAvailability(String productId) async {
final currentProducts = state.value ?? [];
final updatedList = currentProducts.map((product) {
if (product.id == productId) {
return product.copyWith(isAvailable: !product.isAvailable);
}
return product;
}).toList();
state = AsyncValue.data(updatedList);
try {
final updatedProduct = updatedList.firstWhere((p) => p.id == productId);
await _updateProductOnServer(updatedProduct);
} catch (e) {
state = AsyncValue.data(currentProducts);
rethrow;
}
}
// 서버 통신 메소드들
Future<void> _createProductOnServer(Product product) async {
await Future.delayed(const Duration(milliseconds: 500));
// 실제 POST 요청
}
Future<void> _updateProductOnServer(Product product) async {
await Future.delayed(const Duration(milliseconds: 500));
// 실제 PUT 요청
}
Future<void> _deleteProductOnServer(String productId) async {
await Future.delayed(const Duration(milliseconds: 500));
// 실제 DELETE 요청
}
}
final productListProvider = AsyncNotifierProvider<ProductListNotifier, List<Product>>(
ProductListNotifier.new,
);
주의사항과 팁
자주 하는 실수들:
1. build() 메소드에서 예외 처리 안 하기:
// ❌ 잘못된 방법
class BadAsyncNotifier extends AsyncNotifier {
@override
Future build() async {
return await riskyApiCall(); // 예외가 발생하면 AsyncValue.error로 자동 래핑
}
}
// ✅ 올바른 방법 (필요시)
class GoodAsyncNotifier extends AsyncNotifier {
@override
Future build() async {
try {
return await riskyApiCall();
} catch (e) {
// 커스텀 에러 처리가 필요한 경우에만
throw CustomException('데이터 로딩 실패: $e');
}
}
}
2. 낙관적 업데이트 후 복구 안 하기:
// ❌ 잘못된 방법
Future badUpdate() async {
state = AsyncValue.data(newValue);
await apiCall(); // 실패해도 상태 복구 안 함
}
// ✅ 올바른 방법
Future goodUpdate() async {
final previousState = state.value;
state = AsyncValue.data(newValue);
try {
await apiCall();
} catch (e) {
if (previousState != null) {
state = AsyncValue.data(previousState); // 실패 시 복구
}
rethrow;
}
}
성능 최적화 방법:
• 선택적 감시로 불필요한 리빌드 방지
• 불필요한 API 호출 방지
• debounce를 활용한 API 호출 최적화
베스트 프랙티스:
• 낙관적 업데이트 패턴 사용
• 에러 발생 시 사용자 친화적 메시지 제공
• 로딩 상태를 적절히 표시
• 네트워크 에러와 비즈니스 로직 에러 구분
• 리프레시 기능 제공
1. build() 메소드에서 예외 처리 안 하기:
`dart// ❌ 잘못된 방법
class BadAsyncNotifier extends AsyncNotifier
@override
Future
return await riskyApiCall(); // 예외가 발생하면 AsyncValue.error로 자동 래핑
}
}
// ✅ 올바른 방법 (필요시)
class GoodAsyncNotifier extends AsyncNotifier
@override
Future
try {
return await riskyApiCall();
} catch (e) {
// 커스텀 에러 처리가 필요한 경우에만
throw CustomException('데이터 로딩 실패: $e');
}
}
}
`2. 낙관적 업데이트 후 복구 안 하기:
`dart// ❌ 잘못된 방법
Future
state = AsyncValue.data(newValue);
await apiCall(); // 실패해도 상태 복구 안 함
}
// ✅ 올바른 방법
Future
final previousState = state.value;
state = AsyncValue.data(newValue);
try {
await apiCall();
} catch (e) {
if (previousState != null) {
state = AsyncValue.data(previousState); // 실패 시 복구
}
rethrow;
}
}
`성능 최적화 방법:
• 선택적 감시로 불필요한 리빌드 방지
• 불필요한 API 호출 방지
• debounce를 활용한 API 호출 최적화
베스트 프랙티스:
• 낙관적 업데이트 패턴 사용
• 에러 발생 시 사용자 친화적 메시지 제공
• 로딩 상태를 적절히 표시
• 네트워크 에러와 비즈니스 로직 에러 구분
• 리프레시 기능 제공
다른 Provider와 비교
AsyncNotifierProvider vs FutureProvider:
• AsyncNotifierProvider: 비동기 + 상태 관리, 상태 변경 가능, 수동 관리, 수동 구현, CRUD/데이터 조작
• FutureProvider: 일회성 비동기 작업, 상태 변경 불가능, 자동 캐싱, ref.refresh(), 데이터 페칭
AsyncNotifierProvider vs StateNotifierProvider + FutureProvider:
• AsyncNotifierProvider: 하나의 Provider에서 모든 것, 자동 제공, 내장, 보통, 보통
• StateNotifier + Future: 여러 Provider 조합, 수동 관리, 별도 구현, 높음, 높음
선택 기준:
• 비동기 + 상태 관리: AsyncNotifierProvider
• 단순 데이터 페칭: FutureProvider
• 복잡한 동기 상태: NotifierProvider
• 실시간 데이터: StreamProvider
• AsyncNotifierProvider: 비동기 + 상태 관리, 상태 변경 가능, 수동 관리, 수동 구현, CRUD/데이터 조작
• FutureProvider: 일회성 비동기 작업, 상태 변경 불가능, 자동 캐싱, ref.refresh(), 데이터 페칭
AsyncNotifierProvider vs StateNotifierProvider + FutureProvider:
• AsyncNotifierProvider: 하나의 Provider에서 모든 것, 자동 제공, 내장, 보통, 보통
• StateNotifier + Future: 여러 Provider 조합, 수동 관리, 별도 구현, 높음, 높음
선택 기준:
• 비동기 + 상태 관리: AsyncNotifierProvider
• 단순 데이터 페칭: FutureProvider
• 복잡한 동기 상태: NotifierProvider
• 실시간 데이터: StreamProvider
정리
AsyncNotifierProvider는 비동기 작업과 상태 관리를 완벽하게 통합한 Riverpod 2.0의 강력한 솔루션입니다. CRUD 작업이나 복잡한 비동기 플로우를 다룰 때 특히 유용합니다.
핵심 포인트:
• 통합 솔루션: 비동기 로딩과 상태 관리를 하나로 결합
• 자동 상태 관리: 로딩, 성공, 에러 상태 자동 처리
• 낙관적 업데이트: 빠른 UI 반응과 에러 시 복구
• 타입 안전성: 컴파일 타임 에러 방지
시리즈 완료: 이것으로 Flutter Riverpod Provider 종류별 완벽 가이드 시리즈가 완료되었습니다. Provider부터 AsyncNotifierProvider까지 모든 Provider의 특징과 활용법을 마스터하셨습니다!
핵심 포인트:
• 통합 솔루션: 비동기 로딩과 상태 관리를 하나로 결합
• 자동 상태 관리: 로딩, 성공, 에러 상태 자동 처리
• 낙관적 업데이트: 빠른 UI 반응과 에러 시 복구
• 타입 안전성: 컴파일 타임 에러 방지
시리즈 완료: 이것으로 Flutter Riverpod Provider 종류별 완벽 가이드 시리즈가 완료되었습니다. Provider부터 AsyncNotifierProvider까지 모든 Provider의 특징과 활용법을 마스터하셨습니다!