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