MVVM
프로그램을 작성하는 아키텍쳐(패턴, 구조)
상태 관리 라이브러리를 효과적으로, 코드 관리를 효과적으로 관리하기 위해 사용하는 구조.
M: Model
V: View
VM: View Model
- Model: 데이터를 서버에서 가져오고 데이터를 사용할 수 있게 가공하기 위한 클래스를 작성
- Repository: Model의 한 부분으로 데이터를 서버에서 가져오는 담당
- View: UI를 나타내는 부분
- View Model: 데이터 또는 상태를 관리하기 위한 클래스를 작성
사용 방법
1. ProviderScope
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/home_page.dart';
void main() {
// Flutter 위젯트리에서 최상위는 MyApp이므로
// MyApp보다 더 최상위가 돼야 하위의 모든 위젯에서 상태(데이터)에 접근 가능
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: HomePage());
}
}
- 모든 위젯에서 상태 값(데이터)을 가져올 수 있게 Flutter 위젯트리에서 최상위가 되는 곳에 `ProviderScope(child: Widget)`을 사용
2. Model 클래스 작성
데이터를 가공
// lib/user.dart
class User {
String name;
int age;
User({required this.name, required this.age});
// Map -> 인스턴스
User.fromJson(Map<String, dynamic> map)
: this(name: map['name'], age: map['age']);
// 인스턴스 -> Map
Map<String, dynamic> toJson() {
return {'name': name, 'age': age};
}
}
3. Rapository 클래스 작성
데이터를 가져오고 Model클래스를 이용해 인스턴스를 반환
// lib/user_repository.dart
import 'dart:convert';
import 'package:flutter_riverpod_mvvm/user.dart';
class UserRepository {
Future<User> getUser() async {
// 서버에서 데이터를 가져온다는 가정을 위해 1초 딜레이를 줌
await Future.delayed(Duration(seconds: 1));
// 서버에서 가져온 데이터라고 가정
String dummy = '''
{
"name": "jiwon",
"age": 20
}
''';
// 1. jsonDecode로 Map으로 변환
Map<String, dynamic> map = jsonDecode(dummy);
// 2. Map -> 인스턴스로 변환
return User.fromJson(map);
}
}
4. ViewModel 클래스 작성
- 상태를 생성
- 상태를 관리(업데이트)할 뷰모델 생성
- `Notifier<T>`클래스 사용
- 상태 관리를 위해서 만들어진 Riverpod의 클래스
- 이 클래스를 상속받은 클래스(ex. HomeViewModel class)에서 다양한 메서드를 만들어 상태를 관리
- `<T>`는 상태의 타입
- 즉, `T state;`
- `Notifier<T>`클래스 사용
- 뷰에서 상태와 뷰모델을 사용하기 위한 공급자(provider) 생성
- `NotifierProvider<T, E>`객체
- 뷰에서 상태와 `Notifier<T>`클래스를 사용하기 위한 Riverpod의 클래스
- `<T, E>`
- `T`는 상태를 관리할 뷰모델 클래스. 즉, `Notifier<T>`를 상속 받은 클래스
- `E`는 상태의 타입
- `NotifierProvider<T, E>`객체
// lib/home_view_model.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/user.dart';
import 'package:flutter_riverpod_mvvm/user_repository.dart';
/* 뷰모델 - 상태를 업데이트하기 위한 클래스 */
// 1. 관리해야될 상태 클래스 만들기
class HomeState {
// 서버에서 데이터를 가져오지 않은 상태일 수도 있으니 nullable하게 함
User? user;
HomeState(this.user);
}
// 2. 상태를 관리할 뷰모델 만들기 - Notifier 이용
class HomeViewModel extends Notifier<HomeState> {
// build가 반환하는 값은 상태의 초기값
@override
HomeState build() {
return HomeState(null);
}
// 사용자가 버튼 클릭하면 뷰모델은 리포지토리에서 데이터를 가져와서 상태를 업데이트 함
void getUser() async {
UserRepository userRepository = UserRepository();
User user = await userRepository.getUser();
state = HomeState(user);
// 새로운 객체를 state에 할당하지 않으면 자신을 바라보는 View에게
// 상태가 변경되었다고 알리지 않음
// state.user = user // => ❌
}
}
// 3. 뷰모델을 뷰에게 공급해줄 관리자 만들기
// 즉, 위젯에서 뷰모델을 사용하기 위한 변수
// () {return HomeViewModel();} => HomeViewModel.new 로 사용할 수도 있음
// 즉, NotifierProvider<HomeViewModel, HomeState>(HomeViewModel.new);
final homeViewModelProvider = NotifierProvider<HomeViewModel, HomeState>(() {
return HomeViewModel();
});
- ✅ `state = HomeState(user)`에서 `HomeState`로 새로운 인스턴스를 생성해 `state`에 할당하는 이유
- 리렌더링이 되는(상태가 업데이트가 됐다고 판단하는) 이유는 할당 받기 전 `state`의 값과 할당하려는 `state`의 값이 다를 경우이다.
- 그러나 이 앱의 `state`는 `HomeState`클래스에 의해 생성된 객체이다.
- 초기값을 Map타입으로 보면 state = { user: null };
- 객체는 참조를 하므로 `state.user = user`를 하면 할당 받기 전 `state`의 값과 할당하려는 `state`의 값이 같을 수 밖에 없다.
- 그래서 리렌더링을 하지 않기에 새로운 객체를 생성해 `state`에 할당하는 것이다.
- 상태의 타입이 단순히 `int`인 경우에는 `HomeState`와 같은 클래스가 필요하지 않고, 아래처럼 변경하면 된다.
- `Notifier<HomeState>` → `Notifier<int>` 로 변경
- `HomeViewModel`의 `build`의 반환 값을 숫자로 변경
- `state = HomeState(State.counter + 1)` → `state++` 로 변경
5. View 클래스 작성
Consumer 위젯 사용
- `StatelessWidget`이나 `StatefulWidget`에서 사용한다.
- 리렌더링을 위한 `ref.watch`는 `Cousumer`의 `builder`속성 내에서 사용해야 한다.
// lib/home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/home_view_model.dart';
/* view 클래스 */
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Consumer(
builder: (context, ref, child) {
// ✅ watch에 provider변수를 할당하면 '상태'를 반환
// ✅ 즉, 이 '상태'를 구독하기 때문에 업데이트가 일어나면 리렌더링 시작
final homeState = ref.watch(homeViewModelProvider);
return Column(
children: [
Text('name: ${homeState.user?.name}'),
Text('age: ${homeState.user?.age}'),
ElevatedButton(
onPressed: () {
// ✅ read에 provider변수를 할당하면 '상태'를 반환
// final instanceOfHomeState = ref.read(homeViewModelProvider);
// ✅ read에 provider.notifier를 할당하면 '상태를 관리하는' 뷰모델을 반환
final viewModel = ref.read(homeViewModelProvider.notifier);
// ✅✅ 이 부분이 있어야 ref.watch가 동작하면서 리렌더링이 일어남
// ✅✅ viewmodel.getUser의 호출로 viewmodel.getUser내부에 작성한
// ✅✅ state에 업데이트가 일어났기 때문
viewModel.getUser();
},
child: Text('데이터 가져오기'),
),
],
);
},
),
);
}
}
ConsumerWidget 사용
- `StatelessWidget` 대신 사용한다.
- 리렌더링을 위한 `ref.watch`는 `CousumerWidget`의 `build`메서드 내에서 사용해야 한다.
// lib/home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/home_view_model.dart';
/* view 클래스 */
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ✅ ConsumerWidget은 build에 ref가 자동으로 생성됨
// ✅ watch에 provider변수를 할당하면 '상태'를 반환
// ✅ 즉, 이 '상태'를 구독하기 때문에 업데이트가 일어나면 리렌더링 시작
final homeState = ref.watch(homeViewModelProvider);
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
Text('name: ${homeState.user?.name}'),
Text('age: ${homeState.user?.age}'),
ElevatedButton(
onPressed: () {
// ✅ read에 provider변수를 할당하면 '상태'를 반환
// final instanceOfHomeState = ref.read(homeViewModelProvider);
// ✅ read에 provider.notifier를 할당하면 '상태를 관리하는' 뷰모델을 반환
final viewModel = ref.read(homeViewModelProvider.notifier);
// ✅✅ 이 부분이 있어야 ref.watch가 동작하면서 리렌더링이 일어남
// ✅✅ viewmodel.getUser의 호출로 viewmodel.getUser내부에 작성한
// ✅✅ state에 업데이트가 일어났기 때문
viewModel.getUser();
},
child: Text('데이터 가져오기'),
),
],
),
);
}
}
ConsumerWidget, ConsumerStatefulWidget, Consumer 비교
뷰에서 상태나 뷰모델을 가지고 오기 위해서는 Riverpod에서 제공하는 `ref`객체가 있어야 한다.
`ref`객체는 `ConsumerWidget`, `ConsumerStatefulWidget`, `Consumer` 위젯을 통해 접근할 수 있다.
Widget 종류 | 기능 | 사용 상황 |
---|---|---|
`StatelessWidget` | 상태 없음 | 기본 Flutter |
`StatefulWidget` | 상태 있음 | 기본 Flutter |
`ConsumerWidget` | 상태 없음, Provider 사용 | Riverpod |
`ConsumerStatefulWidget` | 상태 있음 + Provider | Riverpod |
`Consumer (Builder)` | 특정 위젯에서만 Provider | Riverpod |
- `ConsumerWidget`
- `StatelessWidget` 대신 사용하는 위젯으로 `setState`는 사용 불가
- 대신 Riverpod을 사용하여 상태 관리가 가능
- `build`메서드의 매개변수로 `ref`가 생성 됨
- `ConsumerStatefulWidget`
- `StatefulWidget` 대신 사용하는 위젯으로 `setState`도 사용 가능
- 당연히 Riverpod을 사용하여 상태 관리도 가능
- `ConsumerState`클래스 내에서는 `ref`객체를 어디에서나 그냥 사용할 수 있게 내부적으로 설계 되어 있음
- `Consumer`
- 업데이트 하기 위한 부분에만 사용하므로 `StatelessWidget`, `StatefulWidget` 두 곳 모두에서 사용 가능
- `setState`와는 관련이 없음
- `bulider`속성의 매개변수로 `ref`가 생성 됨
'TIL(Today I Learned)' 카테고리의 다른 글
[TIL-026] Flutter - Hit Test (0) | 2025.04.14 |
---|---|
[TIL-025] Flutter 위젯 파헤치기 - TextField (0) | 2025.04.14 |
[TIL-023] Dart에서 json사용하기 (0) | 2025.04.10 |
[TIL-022] Flutter 도대체 버튼 스타일링 어떻게 해야 하는거야? (0) | 2025.04.02 |
[TIL-021] Flutter 위젯 파헤치기 - Builder (0) | 2025.04.02 |