1. Repository 패턴과 Provider 조합
가장 기본이 되는 패턴입니다. Repository를 Provider로 관리하여 데이터 접근을 추상화합니다.
Dart
// Repository Provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
final api = ref.read(apiClientProvider);
return UserRepositoryImpl(api);
});
// 데이터 Provider (Repository 사용)
final currentUserProvider = FutureProvider<User?>((ref) async {
final repository = ref.read(userRepositoryProvider);
return repository.getCurrentUser();
});
// UI에서 사용
class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(currentUserProvider);
return userAsync.when(
data: (user) => Text(user?.name ?? 'Guest'),
loading: () => CircularProgressIndicator(),
error: (e, s) => Text('Error: $e'),
);
}
}
2. 검색 필터링 패턴 (Debounce 포함)
사용자 입력에 반응하는 검색 기능의 표준 패턴입니다.
Dart
// 검색어 Provider
final searchQueryProvider = StateProvider<String>((ref) => '');
// Debounced 검색어 Provider
final debouncedSearchProvider = Provider<String>((ref) {
final query = ref.watch(searchQueryProvider);
// Debounce 구현
final timer = Timer(Duration(milliseconds: 500), () {});
ref.onDispose(() => timer.cancel());
return query;
});
// 검색 결과 Provider
final searchResultsProvider = FutureProvider<List<Product>>((ref) async {
final query = ref.watch(debouncedSearchProvider);
if (query.isEmpty) return [];
final repository = ref.read(productRepositoryProvider);
return repository.search(query);
});
// 검색 UI
class SearchBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return TextField(
onChanged: (value) {
ref.read(searchQueryProvider.notifier).state = value;
},
decoration: InputDecoration(hintText: '검색...'),
);
}
}
3. 인증 상태 관리 패턴
거의 모든 앱에서 사용하는 인증 관리 패턴입니다.
Dart
@freezed
class AuthState with _$AuthState {
const factory AuthState({
User? user,
@Default(false) bool isLoading,
String? error,
}) = _AuthState;
}
class AuthNotifier extends StateNotifier<AuthState> {
final AuthRepository _repository;
AuthNotifier(this._repository) : super(const AuthState()) {
_checkAuthStatus();
}
Future<void> _checkAuthStatus() async {
state = state.copyWith(isLoading: true);
try {
final user = await _repository.getCurrentUser();
state = AuthState(user: user);
} catch (e) {
state = AuthState(error: e.toString());
}
}
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true, error: null);
try {
final user = await _repository.login(email, password);
state = AuthState(user: user);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
Future<void> logout() async {
await _repository.logout();
state = const AuthState();
}
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(ref.read(authRepositoryProvider));
});
// 인증 여부 확인 Helper Provider
final isAuthenticatedProvider = Provider<bool>((ref) {
return ref.watch(authProvider).user != null;
});
4. 폼 검증 패턴
입력 폼 검증을 위한 표준 패턴입니다.
Dart
class FormState {
final String email;
final String password;
final Map<String, String> errors;
bool get isValid => errors.isEmpty && email.isNotEmpty && password.isNotEmpty;
}
class FormNotifier extends StateNotifier<FormState> {
FormNotifier() : super(FormState(email: '', password: '', errors: {}));
void setEmail(String email) {
state = state.copyWith(email: email);
_validateEmail();
}
void setPassword(String password) {
state = state.copyWith(password: password);
_validatePassword();
}
void _validateEmail() {
final errors = Map<String, String>.from(state.errors);
if (!state.email.contains('@')) {
errors['email'] = '유효한 이메일을 입력하세요';
} else {
errors.remove('email');
}
state = state.copyWith(errors: errors);
}
void _validatePassword() {
final errors = Map<String, String>.from(state.errors);
if (state.password.length < 6) {
errors['password'] = '비밀번호는 6자 이상이어야 합니다';
} else {
errors.remove('password');
}
state = state.copyWith(errors: errors);
}
}
final formProvider = StateNotifierProvider<FormNotifier, FormState>((ref) {
return FormNotifier();
});
5. 무한 스크롤 (Pagination) 패턴
리스트 화면에서 자주 사용하는 무한 스크롤 패턴입니다.
Dart
class PaginationState<T> {
final List<T> items;
final bool isLoading;
final bool hasMore;
final int page;
final String? error;
const PaginationState({
this.items = const [],
this.isLoading = false,
this.hasMore = true,
this.page = 1,
this.error,
});
}
class PaginatedListNotifier<T> extends StateNotifier<PaginationState<T>> {
final Future<List<T>> Function(int page) fetcher;
PaginatedListNotifier(this.fetcher) : super(PaginationState<T>()) {
loadMore();
}
Future<void> loadMore() async {
if (state.isLoading || !state.hasMore) return;
state = state.copyWith(isLoading: true);
try {
final newItems = await fetcher(state.page);
state = state.copyWith(
items: [...state.items, ...newItems],
page: state.page + 1,
hasMore: newItems.isNotEmpty,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}
void refresh() {
state = PaginationState<T>();
loadMore();
}
}
// 사용 예시
final productListProvider = StateNotifierProvider<
PaginatedListNotifier<Product>,
PaginationState<Product>
>((ref) {
final repository = ref.read(productRepositoryProvider);
return PaginatedListNotifier(
(page) => repository.getProducts(page: page),
);
});
6. 선택 상태 관리 패턴
다중 선택이 필요한 UI에서 사용하는 패턴입니다.
Dart
final selectedItemsProvider = StateProvider<Set<String>>((ref) => {});
// 선택 토글 헬퍼
final itemSelectionProvider = Provider.family<bool, String>((ref, itemId) {
final selectedItems = ref.watch(selectedItemsProvider);
return selectedItems.contains(itemId);
});
// 선택 액션
class SelectionActions {
final WidgetRef ref;
SelectionActions(this.ref);
void toggleItem(String itemId) {
final selected = Set<String>.from(ref.read(selectedItemsProvider));
if (selected.contains(itemId)) {
selected.remove(itemId);
} else {
selected.add(itemId);
}
ref.read(selectedItemsProvider.notifier).state = selected;
}
void selectAll(List<String> itemIds) {
ref.read(selectedItemsProvider.notifier).state = itemIds.toSet();
}
void clearSelection() {
ref.read(selectedItemsProvider.notifier).state = {};
}
}
7. 캐싱과 리프레시 패턴
데이터 캐싱과 강제 새로고침을 관리하는 패턴입니다.
Dart
final productsProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
// 5분간 캐시 유지
ref.cacheFor(Duration(minutes: 5));
final repository = ref.read(productRepositoryProvider);
return repository.getAllProducts();
});
// Pull-to-refresh 구현
class ProductListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsProvider);
return RefreshIndicator(
onRefresh: () async {
// 캐시 무효화 후 재조회
ref.invalidate(productsProvider);
await ref.read(productsProvider.future);
},
child: productsAsync.when(
data: (products) => ListView.builder(...),
loading: () => Center(child: CircularProgressIndicator()),
error: (e, s) => ErrorWidget(e.toString()),
),
);
}
}
8. 글로벌 로딩/에러 처리 패턴
앱 전체의 로딩과 에러를 중앙에서 관리하는 패턴입니다.
Dart
// 글로벌 로딩 상태
final globalLoadingProvider = StateProvider<bool>((ref) => false);
// 글로벌 에러 상태
final globalErrorProvider = StateProvider<String?>((ref) => null);
// 로딩/에러 처리 믹스인
mixin LoadingErrorMixin {
Future<T?> handleAsyncAction<T>(
WidgetRef ref,
Future<T> Function() action,
) async {
ref.read(globalLoadingProvider.notifier).state = true;
ref.read(globalErrorProvider.notifier).state = null;
try {
final result = await action();
return result;
} catch (e) {
ref.read(globalErrorProvider.notifier).state = e.toString();
return null;
} finally {
ref.read(globalLoadingProvider.notifier).state = false;
}
}
}
// 앱 전체 래퍼
class AppWrapper extends ConsumerWidget {
final Widget child;
const AppWrapper({required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isLoading = ref.watch(globalLoadingProvider);
final error = ref.watch(globalErrorProvider);
return Stack(
children: [
child,
if (isLoading)
Container(
color: Colors.black26,
child: Center(child: CircularProgressIndicator()),
),
if (error != null)
Positioned(
top: 50,
left: 16,
right: 16,
child: ErrorBanner(message: error),
),
],
);
}
}
9. 필터와 정렬 조합 패턴
복잡한 리스트 필터링과 정렬을 관리하는 패턴입니다.
Dart
@freezed
class FilterOptions with _$FilterOptions {
const factory FilterOptions({
String? category,
RangeValues? priceRange,
@Default(SortBy.name) SortBy sortBy,
@Default(true) bool ascending,
}) = _FilterOptions;
}
// 필터 상태 Provider
final filterProvider = StateProvider<FilterOptions>((ref) {
return const FilterOptions();
});
// 필터링된 결과 Provider
final filteredProductsProvider = Provider<List<Product>>((ref) {
final products = ref.watch(allProductsProvider);
final filter = ref.watch(filterProvider);
var filtered = products.where((product) {
// 카테고리 필터
if (filter.category != null && product.category != filter.category) {
return false;
}
// 가격 범위 필터
if (filter.priceRange != null) {
if (product.price < filter.priceRange!.start ||
product.price > filter.priceRange!.end) {
return false;
}
}
return true;
}).toList();
// 정렬
filtered.sort((a, b) {
int comparison;
switch (filter.sortBy) {
case SortBy.name:
comparison = a.name.compareTo(b.name);
break;
case SortBy.price:
comparison = a.price.compareTo(b.price);
break;
case SortBy.date:
comparison = a.createdAt.compareTo(b.createdAt);
break;
}
return filter.ascending ? comparison : -comparison;
});
return filtered;
});
10. 의존성 체인 패턴
여러 Provider가 순차적으로 의존하는 패턴입니다.
Dart
// 1. 사용자 정보
final userProvider = FutureProvider<User>((ref) async {
final auth = ref.watch(authProvider);
if (auth.user == null) throw Exception('Not authenticated');
return auth.user!;
});
// 2. 사용자 설정 (사용자 정보 필요)
final userSettingsProvider = FutureProvider<UserSettings>((ref) async {
final user = await ref.watch(userProvider.future);
final repository = ref.read(settingsRepositoryProvider);
return repository.getSettings(user.id);
});
// 3. 테마 설정 (사용자 설정 필요)
final themeProvider = Provider<ThemeData>((ref) {
final settingsAsync = ref.watch(userSettingsProvider);
return settingsAsync.maybeWhen(
data: (settings) => settings.darkMode ? ThemeData.dark() : ThemeData.light(),
orElse: () => ThemeData.light(),
);
});
// 4. 언어 설정 (사용자 설정 필요)
final localeProvider = Provider<Locale>((ref) {
final settingsAsync = ref.watch(userSettingsProvider);
return settingsAsync.maybeWhen(
data: (settings) => Locale(settings.languageCode),
orElse: () => const Locale('ko'),
);
});
실전 활용 팁
Provider 네이밍 컨벤션
Dart
// State를 반환하는 Provider
final cartProvider = StateNotifierProvider<CartNotifier, CartState>(...);
// 단일 값을 반환하는 Provider
final userProvider = FutureProvider<User>(...);
// 파생된 값을 반환하는 Provider
final cartTotalProvider = Provider<double>(...);
// 액션/메서드를 반환하는 Provider
final cartActionsProvider = Provider<CartActions>(...);
폴더 구조
bash
lib/
providers/
auth/ # 인증 관련
products/ # 상품 관련
cart/ # 장바구니 관련
shared/ # 공용 Provider들
features/
auth/ # 인증 화면
products/ # 상품 화면
cart/ # 장바구니 화면
성능 최적화 체크리스트
• select로 필요한 부분만 구독
• family와 autoDispose 적절히 조합
• 무거운 계산은 Provider에서 캐싱
• AsyncValue.guard로 에러 처리 단순화
• 불필요한 Provider 체인 피하기
• family와 autoDispose 적절히 조합
• 무거운 계산은 Provider에서 캐싱
• AsyncValue.guard로 에러 처리 단순화
• 불필요한 Provider 체인 피하기
마무리
이 10가지 패턴이 Flutter Riverpod 실전 개발의 80% 이상을 커버합니다. 각 패턴은 독립적으로 사용할 수도 있고, 조합해서 사용할 수도 있습니다.
핵심은 단순하게 시작하고 필요에 따라 확장하는 것입니다. 모든 패턴을 처음부터 적용하려 하지 말고, 프로젝트의 요구사항에 맞게 선택적으로 사용하세요.
핵심은 단순하게 시작하고 필요에 따라 확장하는 것입니다. 모든 패턴을 처음부터 적용하려 하지 말고, 프로젝트의 요구사항에 맞게 선택적으로 사용하세요.