RiverPod 배우기
오늘은 많은? 회사에서 사용하는 RiverPod에 대해 글을 작성하려고 해요!
provider랑 bloc, GetX는 사용을 해봤지만 Riverpod는 이번이 처음이라 한번 사용한걸 정리하려고 해요
MVVM + Riverpod를 활용한 사용자 정보를 가져와 목록을 보여주는 기능을 예제로 만들어 볼게요
MVVM + Riverpod의 장점
✔ 비즈니스 로직과 UI 분리 → 유지보수가 쉬움
✔ StateNotifierProvider를 활용하여 효율적인 상태 관리
✔ AsyncValue를 활용하여 로딩, 성공, 에러 처리 간편
AsyncValue란? (Flutter Riverpod)
AsyncValue<T>는 Flutter Riverpod에서 비동기 데이터를 안전하고 쉽게 관리할 수 있도록 도와주는 타입입니다.
비동기 API 요청 결과를 로딩, 성공, 에러 상태로 구분하여 처리할 수 있습니다.
기존 방식 불편함
FutureBuilder<List<User>>(
future: fetchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return
} else {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return
},
);
}
},
);
- FutureBuilder 내부에서 로딩, 성공, 에러를 직접 처리해야 해서 코드가 길어짐
- snapshot.hasError 등을 계속 확인해야 함
AsyncValue를 사용하면? (더 깔끔한 코드)
final userProvider = FutureProvider<List<User>>((ref) async {
return fetchUsers();
});
ConsumerWidget(
builder: (context, ref, child) {
final users = ref.watch(userProvider);
return users.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text("Error: $error"),
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return
},
),
);
},
);
- when()을 사용하여 로딩, 성공, 에러 상태를 깔끔하게 분리
- 더 직관적이고 유지보수하기 쉬운 코드
AsyncValue<T>는 세 가지 상태를 가질 수 있습니다.
- 로딩 중 (AsyncValue.loading()) → 데이터를 불러오는 중
- 성공 (AsyncValue.data(value)) → 데이터 불러오기 성공
- 오류 (AsyncValue.error(error, stackTrace)) → 데이터 불러오기 실패
maybeWhen()을 사용하여 일부 상태만 처리
maybeWhen()을 사용하면 특정 상태만 다루고, 나머지는 기본값으로 처리할 수 있습니다.
users.maybeWhen(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) => Text(users[index].name),
),
orElse: () => CircularProgressIndicator(),
);
data 상태만 별도로 처리하고, 나머지는 orElse에서 처리함.
AsyncNotifier와 함께 사용
비동기 상태를 업데이트해야 하는 경우, AsyncNotifier와 함께 사용하면 더욱 효과적입니다.
final userNotifierProvider = AsyncNotifierProvider<UserNotifier, List<User>>(() => UserNotifier());
class UserNotifier extends AsyncNotifier<List<User>> {
@override
Future<List<User>> build() async {
return fetchUsers();
}
Future<void> refreshUsers() async {
state = const AsyncValue.loading();
try {
final users = await fetchUsers();
state = AsyncValue.data(users);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
}
AsyncNotifier를 사용하면 상태를 갱신하거나 API를 다시 호출하는 기능도 쉽게 추가 가능
AsyncValue와 FutureProvider 비교
특징AsyncValueFutureProvider
특징 | AsyncValue | FutureProvider |
비동기 상태 관리 | 지원 | 지원 |
loading, error, data 상태 자동 처리 | 있음 | 없음 |
when()으로 UI 처리 | 가능 | 직접 처리해야 함 |
isLoading, hasError 상태 확인 | 가능 | 불가능 |
state 업데이트 가능 (AsyncNotifier 사용) | 가능 | 불가능 |
- AsyncValue는 Riverpod에서 비동기 상태를 쉽게 관리할 수 있도록 도와줌
- API 호출 결과를 loading, data, error 상태로 구분하여 처리 가능
- when()을 사용하면 UI 코드가 깔끔해지고 유지보수 쉬워짐
- AsyncNotifier와 함께 사용하면 API 호출 및 상태 갱신을 더 쉽게 관리 가능
먼저 riverpod를 추가
https://pub.dev/packages/riverpod
riverpod | Dart package
A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
pub.dev
패키지를 추가후 아래와 같이 만들어 줍니다.
프로젝트 구조로는
lib 밑에
models
viewmodels
views
services
이렇게 세가지를 만들었어요.
- models/ → 데이터 모델
- viewmodels/ → 상태 관리 (Riverpod 사용)
- services/ → API 및 비즈니스 로직
- views/ → UI 화면
그럼 먼저 views/UserScreen.dart 를 만들고아래와 같이 main에 연결합니다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: UserScreen(), // MaterialApp 내부에서 UserScreen 사용
),
);
}
}
ProviderScope가 하는 역할
1. ProviderScope을 추가하면, 모든 Provider들이 사용할 수 있는 전역 상태 저장소가 생성됩니다.
2. 이를 통해 ref.watch(), ref.read(), ref.listen() 등이 정상적으로 작동합니다.
이후 user_screen.dart에 아래의 코드를 넣어주세요.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../viewmodels/user_viewmodel.dart';
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userViewModelProvider);
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: userState.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
),
);
}
}
ref.watch(userViewModelProvider)를 사용하여 ViewModel에서 데이터를 가져옵니다.
when을 사용하여 로딩, 성공, 에러 상태를 구분하여 UI를 렌더링합니다.
※ WidgetRef의 역할 (Flutter Riverpod)
WidgetRef는 Riverpod에서 Provider를 읽고, 감시하고, 관리할 수 있도록 도와주는 객체입니다.
주로 ConsumerWidget 또는 ConsumerStatefulWidget에서 사용됩니다.
메서드 | 설명 | 사용 예시 |
ref.watch(provider) | Provider의 상태를 감지하고 UI를 자동 업데이트 | final count = ref.watch(counterProvider); |
ref.read(provider) | Provider의 현재 값을 가져오지만, UI를 자동 업데이트하지 않음 | ref.read(counterProvider.notifier).state++; |
ref.listen(provider, (previous, next) {}) | Provider 상태가 변경될 때 특정 로직 실행 | ref.listen(counterProvider, (prev, next) => print(next)); |
ref.invalidate(provider) | Provider를 다시 초기화하여 최신 상태로 만듦 | ref.invalidate(userProvider); |
ref.watch() vs ref.read() 차이점
ref.watch(provider) (UI 자동 업데이트)
ref.read(provider) (UI 자동 업데이트 X)
이제 나머지 코드를 만들어 주세요
viewmodels /user_viewmodel.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/user_model.dart';
import '../services/user_service.dart';
final userViewModelProvider = StateNotifierProvider<UserViewModel, AsyncValue<List<User>>>((ref) {
return UserViewModel(UserService());
});
class UserViewModel extends StateNotifier<AsyncValue<List<User>>> {
final UserService _userService;
UserViewModel(this._userService) : super(const AsyncValue.loading()) {
fetchUsers();
}
Future<void> fetchUsers() async {
try {
state = const AsyncValue.loading();
final users = await _userService.fetchUsers();
state = AsyncValue.data(users);
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
Riverpod을 사용하여 상태 관리를 담당하는 ViewModel을 만듭니다.
StateNotifierProvider를 사용하여 상태를 관리합니다.
AsyncValue<List<User>>를 사용하여 로딩, 데이터, 에러 상태를 쉽게 처리할 수 있습니다.
models /user_model.dart
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
사용자 데이터를 표현하는 모델을 생성합니다.
services /user_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/user_model.dart';
class UserService {
Future<List<User>> fetchUsers() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
if (response.statusCode == 200) {
List<dynamic> data = json.decode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users');
}
}
}
실제 API에서 데이터를 가져오는 서비스입니다.
결과