flutter mvvm
| topics | 500-모바일개발 501 Flutter |
| types | 이론 실습 |
| tags | |
| references | ctoahn.tistory.com/15 blog.naver.com/islove8587/220602340150 |
Flutter MVVM
Flutter에서 MVVM 아키텍처 패턴을 적용하는 방법이다.
MVVM이란
- Model: 데이터와 비즈니스 로직
- View: UI 레이어 (Widget)
- ViewModel: View와 Model 사이의 중재자
왜 MVVM을 쓸까: UI 로직과 비즈니스 로직을 분리해서 테스트와 유지보수가 쉬워진다. View는 ViewModel만 알면 되고, ViewModel은 Model만 알면 된다.
폴더 구조
lib/
├── models/
│ └── user.dart
├── repositories/
│ └── user_repository.dart
├── viewmodels/
│ └── user_viewmodel.dart
└── views/
└── user_screen.dart
구현 예시 (Riverpod)
Model
// models/user.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required String email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Repository
// repositories/user_repository.dart
class UserRepository {
final ApiClient _api;
UserRepository(this._api);
Future<User> getUser(String id) async {
final response = await _api.get('/users/$id');
return User.fromJson(response);
}
Future<void> updateUser(User user) async {
await _api.put('/users/${user.id}', user.toJson());
}
}
// Provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepository(ref.watch(apiClientProvider));
});
ViewModel (Notifier)
// viewmodels/user_viewmodel.dart
final userViewModelProvider = AsyncNotifierProvider<UserViewModel, User?>(() {
return UserViewModel();
});
class UserViewModel extends AsyncNotifier<User?> {
@override
Future<User?> build() async {
return null; // 초기값
}
Future<void> loadUser(String id) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repo = ref.read(userRepositoryProvider);
return repo.getUser(id);
});
}
Future<void> updateName(String name) async {
final user = state.value;
if (user == null) return;
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final updated = user.copyWith(name: name);
await ref.read(userRepositoryProvider).updateUser(updated);
return updated;
});
}
}
View
// views/user_screen.dart
class UserScreen extends ConsumerWidget {
final String userId;
const UserScreen({required this.userId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userViewModelProvider);
// 초기 로드
ref.listen(userViewModelProvider, (_, __) {});
useEffect(() {
ref.read(userViewModelProvider.notifier).loadUser(userId);
return null;
}, [userId]);
return Scaffold(
body: userState.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (user) => user == null
? const Text('No user')
: UserContent(user: user),
),
);
}
}
class UserContent extends ConsumerWidget {
final User user;
const UserContent({required this.user, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: [
Text(user.name),
ElevatedButton(
onPressed: () {
ref.read(userViewModelProvider.notifier).updateName('New Name');
},
child: const Text('Update Name'),
),
],
);
}
}
데이터 흐름
┌─────────┐ ┌─────────────┐ ┌────────────┐ ┌─────────┐
│ View │ ───► │ ViewModel │ ───► │ Repository │ ───► │ API │
│(Widget) │ ◄─── │ (Notifier) │ ◄─── │ │ ◄─── │ │
└─────────┘ └─────────────┘ └────────────┘ └─────────┘
watch state 변경 데이터 요청 네트워크
MVVM vs MVC vs MVP
| 패턴 | View-Logic 연결 | 테스트 용이성 |
|---|---|---|
| MVC | Controller가 직접 조작 | 어려움 |
| MVP | Presenter가 View 참조 | 보통 |
| MVVM | 데이터 바인딩 | 쉬움 |
Flutter에서 MVVM이 좋은 이유: 선언적 UI라서 데이터 바인딩 개념과 잘 맞는다. ViewModel의 상태가 바뀌면 Widget이 자동으로 리빌드된다.
관련 문서
- riverpod read listen - Riverpod 상태 관리
- 앱에서의 데이터 사용과 구현 - 데이터 레이어 설계
- freezed,JsonSerialiable - Model 정의