Riverpod — Provider의 진화, 컴파일타임 안전성
Riverpod — Provider의 진화, 컴파일타임 안전성
Riverpod은 Provider 패키지의 저자가 Provider의 한계를 극복하기 위해 새로 만든 상태 관리 라이브러리입니다. 이름도 Provider의 아나그램(글자 재배열)입니다.
Provider vs Riverpod
| 비교 항목 | Provider | Riverpod |
|---|---|---|
| BuildContext 의존 | 필요 | 불필요 |
| 같은 타입 여러 개 | 불가 | 가능 |
| 컴파일타임 안전 | 부분적 | 완전 |
| 테스트 | 위젯 필요 | 쉬움 |
| Provider 조합 | ProxyProvider | ref.watch |
설치
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.5.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.0
riverpod_annotation: ^2.3.0
기본 설정
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// ProviderScope로 전체 앱을 감싸기
const ProviderScope(
child: MyApp(),
),
);
}
Provider 종류
Provider (읽기 전용)
// 변하지 않는 값
final greetingProvider = Provider<String>((ref) {
return '안녕하세요';
});
StateProvider (단순 상태)
// 단순한 값 하나를 관리
final counterProvider = StateProvider<int>((ref) => 0);
// 사용
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Icon(Icons.add),
),
);
}
}
StateNotifierProvider (복잡한 상태)
// 상태 모델
class TodoState {
final List<Todo> todos;
final bool isLoading;
const TodoState({this.todos = const [], this.isLoading = false});
TodoState copyWith({List<Todo>? todos, bool? isLoading}) {
return TodoState(
todos: todos ?? this.todos,
isLoading: isLoading ?? this.isLoading,
);
}
}
// StateNotifier
class TodoNotifier extends StateNotifier<TodoState> {
TodoNotifier() : super(const TodoState());
void add(String title) {
state = state.copyWith(
todos: [...state.todos, Todo(title: title)],
);
}
void toggle(int index) {
final newTodos = [...state.todos];
newTodos[index] = newTodos[index].copyWith(
isDone: !newTodos[index].isDone,
);
state = state.copyWith(todos: newTodos);
}
}
// Provider 선언
final todoProvider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
return TodoNotifier();
});
FutureProvider (비동기 데이터)
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get(Uri.parse('/api/user'));
return User.fromJson(jsonDecode(response.body));
});
// 사용 — AsyncValue로 로딩/에러/데이터 처리
class UserScreen extends ConsumerWidget {
const UserScreen({super.key});
@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) => Text('이름: ${user.name}'),
);
}
}
StreamProvider
final messagesProvider = StreamProvider<List<Message>>((ref) {
return firestore.collection('messages').snapshots().map(
(snapshot) => snapshot.docs.map(Message.fromDoc).toList(),
);
});
ConsumerWidget vs Consumer
// ConsumerWidget: 전체 위젯이 리빌드
class MyScreen extends ConsumerWidget {
const MyScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
// Consumer: 특정 부분만 리빌드 (성능 최적화)
class MyScreen2 extends StatelessWidget {
const MyScreen2({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('리빌드 안 됨'),
Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('$count'); // 이 부분만 리빌드
},
),
],
);
}
}
// ConsumerStatefulWidget: StatefulWidget에서 Riverpod 사용
class MyScreen3 extends ConsumerStatefulWidget {
const MyScreen3({super.key});
@override
ConsumerState<MyScreen3> createState() => _MyScreen3State();
}
class _MyScreen3State extends ConsumerState<MyScreen3> {
@override
void initState() {
super.initState();
// ref 사용 가능
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
ref.watch vs ref.read vs ref.listen
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch: 값이 변할 때 리빌드 (build 안에서 사용)
final count = ref.watch(counterProvider);
// listen: 값이 변할 때 사이드 이펙트 실행
ref.listen<int>(counterProvider, (previous, next) {
if (next == 10) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('10에 도달!')),
);
}
});
return ElevatedButton(
// read: 현재 값만 읽기 (콜백 안에서 사용)
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('$count'),
);
}
Provider 간 의존성
final authProvider = StateProvider<String?>((ref) => null);
// 다른 Provider의 값에 의존
final apiServiceProvider = Provider<ApiService>((ref) {
final token = ref.watch(authProvider);
return ApiService(token: token);
});
// apiServiceProvider에 의존하는 데이터 Provider
final postsProvider = FutureProvider<List<Post>>((ref) async {
final api = ref.watch(apiServiceProvider);
return api.fetchPosts();
});
authProvider가 변경되면 apiServiceProvider와 postsProvider도 자동으로 갱신됩니다.
family와 autoDispose
// family: 매개변수를 받는 Provider
final userByIdProvider = FutureProvider.family<User, int>((ref, userId) async {
return await fetchUser(userId);
});
// 사용
final user = ref.watch(userByIdProvider(42));
// autoDispose: 더 이상 사용되지 않을 때 자동 해제
final searchProvider = FutureProvider.autoDispose<List<Item>>((ref) async {
// 화면을 떠나면 자동으로 폐기됨
return await searchItems();
});
정리
- Riverpod은 BuildContext 없이도 상태에 접근할 수 있어 테스트가 쉽습니다
AsyncValue.when()으로 로딩/에러/데이터를 깔끔하게 처리할 수 있습니다ref.watch(빌드),ref.read(콜백),ref.listen(사이드 이펙트)을 구분해서 사용하세요- Provider 간 의존성이 자동으로 관리되는 것이 큰 장점입니다
family로 매개변수를 받고,autoDispose로 메모리를 관리합니다
댓글 로딩 중...