Riverpod 개요
Riverpod은 Flutter의 상태 관리를 위한 강력한 패키지로, 기존의 Provider 패턴을 기반으로 하지만 더 유연하고 예측 가능한 상태 관리를 지원합니다.
- 컴파일 타임 오류 방지: Riverpod의 Provider들은 데이터 타입을 명시해야 합니다. 타입이 맞지 않으면 코드가 컴파일되지 않으므로, 런타임에서 발생할 수 있는 타입 오류를 사전에 방지할 수 있습니다.
- 의존성 주입(DI): 상태 간의 의존 관계를 명확히 설정하고 관리할 수 있습니다.
- 가벼운 초기화와 상태 재사용: 필요할 때만 상태를 생성하고 재사용하는 구조로 메모리 관리에 이점이 있습니다.
의존성 주입(DI)
Riverpod에서 의존성 주입(DI)은 Provider를 통해 서로 다른 상태나 객체 간의 관계를 명확하게 설정하는 것을 의미합니다. 이는 특히 Provider에 필요한 인스턴스를 다른 Provider에서 주입하거나 참조할 수 있게 해 주기 때문에 다양한 상태를 필요로 하는 Flutter 앱에서 매우 유용합니다.
final databaseProvider = Provider<Database>((ref) => Database());
final userRepositoryProvider = Provider<UserRepository>((ref) {
final database = ref.watch(databaseProvider); // databaseProvider 의존성 주입
return UserRepository(database);
});
userRepositoryProvider
는 databaseProvider
에 의존하고 있으며, 이 관계는 컴파일 타임에 명확하게 확인할 수 있어 실수를 줄일 수 있습니다.
Provider 종류
- Provider
- 기본 제공자로, 상태가 변하지 않는 값을 제공할 때 사용합니다.
- 사용 예: API 클라이언트, 설정 값 등 앱 실행 중 변하지 않는 값이나 객체 제공.
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient();
});
- FutureProvider
- 비동기 데이터를 처리하는 Provider로, Future를 반환하는 비동기 작업의 상태를 관리합니다.
- 사용 예: API 호출, 파일 읽기 등 비동기 작업에 사용.
final userDataProvider = FutureProvider<User>((ref) async {
final apiClient = ref.read(apiClientProvider);
return await apiClient.fetchUserData();
});
- StreamProvider
- 스트림 데이터를 제공하는 Provider로, Stream을 반환하는 데이터 스트림을 관리합니다.
- 사용 예: 실시간 데이터 스트림, Firebase와의 실시간 상호작용 등에 사용.
final messagesProvider = StreamProvider<List<Message>>((ref) {
final apiClient = ref.read(apiClientProvider);
return apiClient.getMessageStream();
});
- StateProvider
- 간단한 상태 관리에 유용하며, 주로 기본 데이터 타입(int, String 등)을 사용하여 상태를 변경할 수 있습니다.
- 사용 예: 카운터, 입력 필드 등 간단한 상태 관리에 적합.
final counterProvider = StateProvider<int>((ref) => 0);
// 상태 업데이트
ref.read(counterProvider).state++;
5. NotifierProvider
- Notifier 클래스를 기반으로 하는 상태 관리 Provider로, 객체 기반의 상태를 관리하기에 적합합니다.
- 사용 예: 복잡한 비즈니스 로직이나 데이터 모델을 처리할 때.
- 장점: Notifier를 통해 상태와 비즈니스 로직을 캡슐화하고 재사용 가능한 형태로 관리할 수 있습니다.
final counterProvider = NotifierProvider<CounterNotifier, int>(() {
return CounterNotifier();
});
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
Consumer(builder: (context, ref, _) {
final count = ref.watch(counterProvider); // 상태 읽기
final counter = ref.read(counterProvider.notifier); // 상태 변경자
return Column(
children: [
Text('Count: $count'),
Row(
children: [
IconButton(
icon: Icon(Icons.add),
onPressed: () => counter.increment(),
),
IconButton(
icon: Icon(Icons.remove),
onPressed: () => counter.decrement(),
),
],
),
],
);
});
6.AsyncNotifierProvider 정의
- 비동기 작업을 위한 AsyncNotifier 기반 Provider로, 비동기 데이터를 처리할 때 사용합니다.
- 사용 예: API 호출, 비동기 파일 작업 등.
final userProvider = AsyncNotifierProvider<UserNotifier, User>(() {
return UserNotifier();
});
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
// 예: 비동기 API 호출로 사용자 데이터 가져오기
return await fetchUser();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => fetchUser());
}
}
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// userProvider의 상태를 구독
final userAsyncValue = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(title: Text('User Information')),
body: userAsyncValue.when(
// 데이터가 로딩 중일 때 로딩 인디케이터 표시
loading: () => Center(child: CircularProgressIndicator()),
// 데이터가 성공적으로 로드되었을 때
data: (user) => Center(child: Text('User: ${user.name}')),
// 데이터 로드 중 오류가 발생했을 때
error: (error, stackTrace) => Center(child: Text('Error: $error')),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(userProvider.notifier).refresh(),
child: Icon(Icons.refresh),
),
);
}
}
StateNotifer → Notifier 마이그레이션
기존의 StateNotifier
는 생성자에서 초기 상태를 정의하고 메서드를 통해 상태를 관리했지만, Notifier는 build
메서드를 사용해 더 단순하게 초기 상태를 정의합니다.
변경 전 (StateNotifier)
class CounterStateNotifier extends StateNotifier<int> {
CounterStateNotifier() : super(0);
void increment() => state++;
}
final counterProvider = StateNotifierProvider<CounterStateNotifier, int>((ref) {
return CounterStateNotifier();
});
변경 후 (Notifier)
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(() {
return CounterNotifier();
});
AsyncValue.gaurd() – try catch 생략
AsyncValue.guard
를 사용하면, 비동기 작업이 성공할 경우 상태를 AsyncValue.data
로, 실패할 경우 AsyncValue.error
로 자동으로 설정해줍니다.
오류가 발생할 경우: catch
블록으로 예외를 잡아 AsyncValue.error(error, stackTrace)
로 감싸서 오류 상태로 반환합니다.
guard
메서드는 비동기 함수인 computation
을 파라미터로 받습니다.
computation
을 실행할 때 await
로 결과를 받아옵니다.
성공할 경우: AsyncValue.data(result)
로 감싸서 성공 상태로 반환합니다.