CH03 MVVM
MVVM ์ ๊ดํ ์ดํด
์ฌ์ค ์์๊ฐ VVMM ์ธ๋ฐ, ์ MVVM ์ผ๋ก ์ฉ์ด๊ฐ ์ ๋ฆฝ ๋์๋์ง ๋ชจ๋ฅด๊ฒ ๋ค. ์ด ํฌ์คํ ์ ๋ณด๊ณ ํ์ตํ๋ค.
์ ๋ง ๊ฐ๋จํ ์ ๋ฆฌํ์๋ฉด View๋ ๋ง ๊ทธ๋๋ก View ๋ค. ์ฌ์ฉ์์ ์ํธ์์ฉ ํ๋ฉด์ ์ฌ์ฉ์์ action ์ ViewModel ๋ก ์ ๋ฌํ๋ ์ญํ ๊ณผ, ViewModel ์ผ๋ก๋ถํฐ ์๋ต์ ๋ฐ์ ์ฌ์ฉ์์๊ฒ ์ ๋ฌํ๋ ๊ฒ์ ์ฑ ์์ง๋ค. ์ฌ๊ธฐ์ ํ์คํ ํด์ผํ ๊ฒ์ View ๋ ์ํ๊ด๋ฆฌ๋ฅผ ํ์ง ์๋๋ค๋ ๊ฒ์ด๋ค.
ViewModel ์ View ์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋ฉด์ ์ํ๊ด๋ฆฌ๋ ๋ด๋นํ๋ค.
Model ์ data access layer ์ ๋์ผํ ์ญํ ์ด๋ค.
MVVM ๊ธฐ๋ณธ ํจํด
์ ํฌ๋ธ ์ ๊ด์ฐฎ์ ๊ฐ์๊ฐ ์์ด ์ฐธ๊ณ ํ๋ค. udemy ๊ฐ์์์๋ ๊ณ์ธต์ด ๋๋ฌด ๋ถ๋ฆฌ๊ฐ ๋์ด์ ์ผ๋จ MVVM ์์ฒด์ ๋ํ ์ดํด๋ฅผ ๋ํ๊ธฐ ์ํด์ ๊ธฐ๋ณธ ํจํด์ ๋จผ์ ํ์ตํ๋ค.
import 'package:flutter/material.dart';
import 'package:flutter_clean_architecture/presentation/simple_mvvm/simple_view_model.dart';
class SimpleScreen extends StatefulWidget {
@override
_SimpleScreenState createState() => _SimpleScreenState();
}
class _SimpleScreenState extends State<SimpleScreen> {
SimpleViewModel viewModel = SimpleViewModel();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
StreamBuilder(
stream: viewModel.mvvmStream,
builder: (context, snapshot) {
print('StreamBuilder > build > snapshot : ${snapshot.data}');
int count = 0;
if (snapshot.data != null) {
count = snapshot.data!.count;
}
return Center(
child: Text(
count.toString(),
style: TextStyle(fontSize: 30),
),
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
viewModel.increaseCounter();
},
icon: Icon(Icons.exposure_plus_1),
),
IconButton(
onPressed: () {
viewModel.decreaseCounter();
},
icon: Icon(Icons.exposure_minus_1),
),
],
),
],
),
),
);
}
}
import 'dart:async';
import 'package:flutter_clean_architecture/presentation/simple_mvvm/simple_model.dart';
class SimpleViewModel {
late SimpleModel _model;
final StreamController<SimpleModel> _streamController =
StreamController<SimpleModel>();
Stream<SimpleModel> get mvvmStream => _streamController.stream;
SimpleViewModel() {
_model = SimpleModel();
}
void update() {
_streamController.sink.add(_model);
}
void increaseCounter() {
_model.count++;
update();
}
void decreaseCounter() {
_model.count--;
update();
}
}
class SimpleModel {
int count = 0;
@override
String toString() {
return 'SimpleModel{count: $count}';
}
}
Provider ์์ notifyListeners(); ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์๊ณผ ์ ์ฌํ๋ค. ๊ฒฐ๊ตญ Stream ๋ Publish, Subscribe ์๋ฆฌ์ด๋ฉฐ ์ด๋ฅผ ์ด์ฉํด์ ํด๋น Stream ์ ๊ตฌ๋ ํ๋ StreamBuilder ๋ฅผ ํตํด์ ๋ณ๊ฒฝ์ด ์์๋๋ง๋ค ์ด๋ฅผ ๋ฐ์ rebuild ํ๋ ๋ฐฉ์์ด๋ค.
์ ํฌ๋ธ ๊ฐ์์๋ Provider ๋ฅผ ์ฌ์ฉํ MVVM ๋ ์์๋๋ฐ, ์ฌ์ค ๋ช ์นญ์ ์ด๋ ๊ฒ ๊ฐ๋ค ๋ถ์ฌ์ ๋ค๋ฅธ ๊ฒ ๊ฐ์ง๋ง ๊ทธ๋ฅ ์ผ๋ฐ์ ์ธ Provider ์๋ค. View ์ ๋๋ฉ์ธ ๋ก์ง์ ์ ๋ถ Provider ์ ์์ํ๊ณ Provider ์ ์ฒ๋ฆฌ์ ๋ฐ๋ผ์ View ๋ rebuild ๊ฐ ํ์ํ ๋ ์์์ rebuild ๊ฐ ๋๋๋ก(react) Provider ๋ฅผ watch ํ๋ ํํ์ธ ๊ฒ์ด๋ค. ์ด๊ฑธ ๊ธฐ๋ณธํ ๋งฅ๋ฝ์์ ๋ณด๋ฉด StreamBuilder ๋ฅผ ํตํด์ rebuild ๋๋ ๊ฒ๊ณผ ๋๊ฐ์ ๋ฐฉ์์ด๋ค.
๊ฐ์์์ ์ฌ์ฉ๋ MVVM ํจํด
์์ค์ฝ๋ ๊ฐ ๋๋ฌด ๊ธธ์ด์ ๋งํฌ๋ง ๋จ๊ธด๋ค.
์ ๊ธฐ๋ณธํ๊ณผ ๋น์ทํ์ง๋ง ๊ฐ์ฅ ํฌ๊ฒ ๋ค๋ฅธ ๋ถ๋ถ์ ๊ณ์ธต์ด ๋ ์ธ๋ถํ ๋์ด ์๋ค๋ ๊ฒ์ด๋ค. ๋ํ์ ์ผ๋ก ์๋ ํด๋์ค๊ฐ ์๋ค.
import 'dart:async';
import 'package:complete_advanced_flutter/presentation/common/state_renderer/state_render_impl.dart';
import 'package:rxdart/rxdart.dart';
abstract class BaseViewModel extends BaseViewModelInputs
with BaseViewModelOutputs {
StreamController _inputStateStreamController =
BehaviorSubject<FlowState>();
@override
Sink get inputState => _inputStateStreamController.sink;
@override
Stream<FlowState> get outputState =>
_inputStateStreamController.stream.map((flowState) => flowState);
@override
void dispose() {
_inputStateStreamController.close();
}
// shared variables and functions that will be used through any view model.
}
abstract class BaseViewModelInputs {
void start(); // will be called while init. of view model
void dispose(); // will be called when viewmodel dies.
Sink get inputState;
}
abstract class BaseViewModelOutputs {
Stream<FlowState> get outputState;
}
BaseViewModel ๋ผ๋ ์ต์์ class ๋ฅผ ๋ง๋ค๊ณ ๋ชจ๋ ViewModel ์ด ์ด๋ฅผ ์์ํ๋๋ก ํ๋ค. ๊ฐ ViewModel ์์๋ ๋ ๊ณ์ธต์ ์๋์ ๊ฐ์ด ๋๋๋ค.
import 'dart:async';
import 'package:complete_advanced_flutter/domain/model/model.dart';
import 'package:complete_advanced_flutter/presentation/base/baseviewmodel.dart';
import 'package:complete_advanced_flutter/presentation/resources/assets_manager.dart';
import 'package:complete_advanced_flutter/presentation/resources/strings_manager.dart';
import 'package:easy_localization/easy_localization.dart';
class OnBoardingViewModel extends BaseViewModel
with OnBoardingViewModelInputs, OnBoardingViewModelOutputs {
// stream controllers
final StreamController _streamController =
StreamController<SliderViewObject>();
late final List<SliderObject> _list;
int _currentIndex = 0;
// inputs
@override
void dispose() {
_streamController.close();
}
@override
void start() {
_list = _getSliderData();
// send this slider data to our view
_postDataToView();
}
@override
int goNext() {
int nextIndex = _currentIndex++; // +1
if (nextIndex >= _list.length) {
_currentIndex = 0; // infinite loop to go to first item inside the slider
}
return _currentIndex;
}
@override
int goPrevious() {
int previousIndex = _currentIndex--; // -1
if (previousIndex == -1) {
_currentIndex =
_list.length - 1; // infinite loop to go to the length of slider list
}
return _currentIndex;
}
@override
void onPageChanged(int index) {
_currentIndex = index;
_postDataToView();
}
@override
Sink get inputSliderViewObject => _streamController.sink;
// outputs
@override
Stream<SliderViewObject> get outputSliderViewObject =>
_streamController.stream.map((slideViewObject) => slideViewObject);
// private functions
List<SliderObject> _getSliderData() => [
SliderObject(
AppStrings.onBoardingTitle1.tr(),
AppStrings.onBoardingSubTitle1.tr(),
ImageAssets.onboardingLogo1),
SliderObject(
AppStrings.onBoardingTitle2.tr(),
AppStrings.onBoardingSubTitle2.tr(),
ImageAssets.onboardingLogo2),
SliderObject(
AppStrings.onBoardingTitle3.tr(),
AppStrings.onBoardingSubTitle3.tr(),
ImageAssets.onboardingLogo3),
SliderObject(
AppStrings.onBoardingTitle4.tr(),
AppStrings.onBoardingSubTitle4.tr(),
ImageAssets.onboardingLogo4)
];
_postDataToView() {
inputSliderViewObject.add(
SliderViewObject(_list[_currentIndex], _list.length, _currentIndex));
}
}
// inputs mean the orders that our view model will recieve from our view
abstract class OnBoardingViewModelInputs {
void goNext(); // when user clicks on right arrow or swipe left.
void goPrevious(); // when user clicks on left arrow or swipe right.
void onPageChanged(int index);
Sink
get inputSliderViewObject; // this is the way to add data to the stream .. stream input
}
// outputs mean data or results that will be sent from our view model to our view
abstract class OnBoardingViewModelOutputs {
Stream<SliderViewObject> get outputSliderViewObject;
}
class SliderViewObject {
SliderObject sliderObject;
int numOfSlides;
int currentIndex;
SliderViewObject(this.sliderObject, this.numOfSlides, this.currentIndex);
}
์ด๋ ๊ฒ ViewModel ์ด ์๋ .dart ์ ์์ ๊ฐ์ด abstract class ๋ฅผ ๋ ๋ง๋ค์ด์ BaseViewModel ์ ์์ํ๊ฒ ํ๋ค ์ ๊ฒ๋ค์ ๋ค ๊ฐ์ด mixin ํ๋ค.
goNext, goPrevious, onPageChanged ๋ ํด๋น ViewModel ํน์ ์ ๊ฒ์ด๋ผ์ ViewModel ์ ์ ์ธํด๋ ๋์ง๋ง ์๋ง๋ ๋ช ์์ ์ผ๋ก ~Input ๋ด์ ๋ฃ์ด์ฃผ์ด์ ํ์คํ๊ฒ ๋ถ๋ฆฌ์ํค๋ ค๋ ์๋๋ก ๋ณด์ธ๋ค.
~Input ๋ด์ Sink ๋ก ์ค์ ํด ์ค ๊ฒ์ ์ฌ์ค ๊ฒฐ๊ตญ StreamController ์ sink ์ธ๋ฐ, ์๋ฏธ๋ View -> ViewModel ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌ(์ํธ์์ฉ)ํ ๋ ๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ๋ ์ ๊ตฌ๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค. ๊ฐ์ธ์ ์ผ๋ก ๋ฐ๋ก ๋ถ๋ฆฌํ์ง ๋ง๊ณ ~controller.sink ๋ก ๊ทธ๋๋ก ์ฌ์ฉํ๋ ๊ฒ์ด ํจ์ฌ ํด์์ด ๋จ์ํ๊ณ ์ข์ ๊ฒ ๊ฐ๋ค. ๋จ์ํ ViewModel ์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ stream ์ผ๋ก publish ํ ๋ ๊ณผ์ ์์ โ๋ฐ๋โ input ๊ฐ๋ ์ด๋ค.
Stream ์ ํ์ฉํ View, ViewModel ์ํธ์์ฉ ์ดํด
์๋ ๊ทธ๋ฆผ์ผ๋ก ๋ง๋ค์๋ค. ๊ฐ์ฌ์ ์ํ ์ฝ๋๊ฐ ์ค์ํ ๊ฒ์ด ์๋๋ผ ์๋ ๊ทธ๋ฆผ์ด ๊ณจ์๋ผ๋ ๊ฒ์ ์๊ณ ์ดํดํ๋ค.

Stream ์ ํ์ํ๋ค๋ฉด ์ฌ๋ฌ ๊ฐ๊ฐ ๋ ์ ์๋ค. ๊ฐ์์์๋ ๋ก๊ทธ์ธ์ name, password, validation, isLoginSuccess ๊ฐ๊ฐ์ ์ํ ๋ค ๊ฐ์ StreamController ๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ๊ณ ์๋ค. ๊ฐ์ ์ฝ๋๊ฐ ์ข ๊น๋ํ์ง ๋ชปํ๋ฏ๋ก View-ViewModel ๊ฐ์ Stream ์ด ๋ค์์ผ ์ ์๋ค๋ ๊ฒ๋ง ๊ธฐ์ตํ์. ๋ค์์ธ ์ํฉ์ด ์คํ๋ ค ๋ง์ ๊ฒ ๊ฐ๋ค.
Last updated