🥖 Bread Basics/Flutter
[Flutter] Provider 상태관리:: ChangeNotifier를 더 체계적으로
BreadDev
2025. 6. 27. 14:05
728x90
이전 포스트에서는 MVVM ChangeNotifier 패턴으로 화면과 로직을 분리하는 방법을 배웠습니다. 하지만 코드를 작성하면서 "ViewModel을 직접 생성해야 하고, ListenableBuilder가 조금 번거롭다"는 점이 약간의 아쉬움이 있었습니다.
오늘은 Provider 라이브러리를 도입해서 공부하고, ChangeNotifier를 더 깔끔하게 관리해보겠습니다!
MVVM ChangeNotifier의 아쉬움들
class MvvmChangeNotifierHomePage extends StatelessWidget {
MvvmChangeNotifierHomePage({super.key});
final viewModel = CounterViewModel(); // 매번 직접 생성해야 함
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel, // 타입 정보가 명확하지 않음
builder: (context, child) {
return Column(
children: [
Text('Counter: ${viewModel.count}'),
ElevatedButton(
onPressed: () => viewModel.countUp(),
child: Text('증가'),
),
],
);
}
);
}
}
느꼈던 불편함들:
- 직접 생성: final viewModel = CounterViewModel() 반복
- 타입 안전성: ListenableBuilder에서 타입이 명확하지 않음
Provider를 도입한 후 코드가 어떻게 변했는지 살펴보겠습니다.
ViewModel은 동일 (변경 없음)
class CounterViewModel extends ChangeNotifier {
// 데이터
int count = 0;
CounterViewModel();
// 로직
void countUp() {
print('CounterViewModel - countUp count: $count');
count = count + 1;
notifyListeners();
}
void countDown() {
count = count - 1;
notifyListeners();
}
}
main.dart: Provider로 감싸기
void main() {
runApp(
ChangeNotifierProvider<CounterViewModel>(
create: (context) {
return CounterViewModel(); // Provider가 관리
},
child: const MvvmProviderCounterApp(),
)
);
}
변화:
- ChangeNotifierProvider: ViewModel 생성을 Provider에게 위임
- create 함수: ViewModel 인스턴스를 만드는 방법 정의
- 앱 레벨에서 설정: 한 번 설정으로 끝
View: Consumer로 깔끔하게
class MvvmProviderCounterHomePage extends StatelessWidget {
const MvvmProviderCounterHomePage({super.key}); // const 추가 가능!
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MMVM Provider Counter App')),
body: SafeArea(
child: Center(
// Consumer: 타입 안전하고 명확함
child: Consumer<CounterViewModel>(
builder: (context, viewModel, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [
Text(
'Counter.count : ${viewModel.count}',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
ElevatedButton(
onPressed: () => viewModel.countUp(),
child: Text('카운트 증가'),
),
ElevatedButton(
onPressed: () => viewModel.countDown(),
child: Text('카운트 감소'),
),
],
);
}
)
),
),
);
}
}
실제 변화 분석: Before vs After
Widget 생성자
// Before: 인스턴스 생성으로 인한 제약
class HomePage extends StatelessWidget {
HomePage({super.key}); // const 불가능
final viewModel = CounterViewModel(); // 인스턴스 생성
}
// After: 깔끔한 생성자
class HomePage extends StatelessWidget {
const HomePage({super.key}); // const 가능!
// 인스턴스 생성 코드 없음
}
상태 구독 방식
// Before: ListenableBuilder
ListenableBuilder(
listenable: viewModel, // 타입이 명시적이지 않음
builder: (context, child) {
// viewModel 타입을 추론해야 함
return Text('${viewModel.count}');
}
)
// After: Consumer (타입 명시)
Consumer<CounterViewModel>(
builder: (context, viewModel, child) {
// viewModel이 CounterViewModel 타입임이 명확
return Text('${viewModel.count}');
}
)
앱 구조
// Before: 각 위젯에서 개별 관리
void main() {
runApp(const MvvmChangeNotifierApp());
}
class HomePage extends StatelessWidget {
final viewModel = CounterViewModel(); // 위젯마다 생성
}
// After: 중앙 집중식 관리
void main() {
runApp(
ChangeNotifierProvider<CounterViewModel>(
create: (context) => CounterViewModel(), // 한 곳에서 생성
child: const MvvmProviderCounterApp(),
)
);
}
class HomePage extends StatelessWidget {
// Provider에서 가져다 쓰기만 함
}
다음 단계로의 발전 방향
🚀 Provider로 할 수 있는 더 많은 것들
// 1. 여러 화면에서 상태 공유
class Page1 extends StatelessWidget {
Widget build(context) {
return Consumer<CounterViewModel>(...); // 같은 상태
}
}
class Page2 extends StatelessWidget {
Widget build(context) {
return Consumer<CounterViewModel>(...); // 같은 상태
}
}
// 2. context.read() / context.watch() 활용
class MyWidget extends StatelessWidget {
Widget build(context) {
final count = context.watch<CounterViewModel>().count;
return ElevatedButton(
onPressed: () => context.read<CounterViewModel>().countUp(),
child: Text('Count: $count'),
);
}
}
// 3. Selector로 최적화
Selector<CounterViewModel, int>(
selector: (context, vm) => vm.count,
builder: (context, count, child) {
return Text('Count: $count'); // count만 변경될 때만 리빌드
},
)
마무리
🎯 실제 개선된 점들:
- ListenableBuilder → Consumer: 타입 안전성과 명확성
- 인스턴스 생성 위치: Widget → main.dart로 중앙화
- const 생성자: 성능 최적화 가능