- PVSM.RU - https://www.pvsm.ru -
Я начала изучать Flutter и недавно потратила целый день пытаясь внедрить архитектуру Model-View-ViewModel в свое приложение на Flutter. Обычно я пишу под Андроид на Java, MVVM реализую с помощью AndroidViewModel и LiveData/MutableLiveData. То есть опыт программирования и применения паттерна есть, приложение — простой таймер. Так что ничто не предвещало столь больших временных затрат на простую задачу.
Поиски статей и инструкций по MVVM во Flutter (без использования RxDart) дали один пример [1] причем без ссылки на полный исходник, так что хочу немного облегчить для интересующихся изучение этого паттерна во Flutter.
Проект без MVVM [2] представляет собой один экран с таймером обратного отсчета. По нажатию на кнопку таймер запускается или ставится на паузу в зависимости от состояния. Когда время заканчивается, выдается уведомление или проигрывается звук.
Приступим к внедрению MVVM, сначала я описала интерфейс, который мне потребуется для взаимодействия между виджетом и моделью (создан файл timer_view_model.dart):
abstract class TimerViewModel {
Stream<bool> get timerIsActive;
Stream<String> get timeTillEndReadable;
Stream<bool> get timeIsOver;
void changeTimerState();
}
То есть я хочу получать события изменения состояния кнопки (остановить таймер – продолжить), знать, когда таймер закончился, получать время, которое надо отобразить на экране. Еще хочу останавливать/запускать таймер. Строго говоря, описание этого интерфейса необязательно, здесь я просто хочу показать, что требуется от модели.
Далее реализация модели – файл timer_view_model_impl.dart
Таймер работает по факту как StreamController с одним подписчиком. Основа для кода взята из вот этой статьи [3]. Там как раз есть описание контроллера, который работает по таймеру и его можно приостанавливать и запускать снова. В общем почти идеальное совпадение. Измененный под мою задачу код:
static Stream<DateTime> timedCounter(Duration interval, Duration maxCount) {
StreamController<DateTime> controller;
Timer timer;
DateTime counter = new DateTime.fromMicrosecondsSinceEpoch(maxCount.inMicroseconds);
void tick(_) {
counter = counter.subtract(oneSec);
controller.add(counter); // Ask stream to send counter values as event.
if (counter.millisecondsSinceEpoch == 0) {
timer.cancel();
controller.close(); // Ask stream to shut down and tell listeners.
}
}
void startTimer() {
timer = Timer.periodic(interval, tick);
}
void stopTimer() {
if (timer != null) {
timer.cancel();
timer = null;
}
}
controller = StreamController<DateTime>(
onListen: startTimer,
onPause: stopTimer,
onResume: startTimer,
onCancel: stopTimer);
return controller.stream;
}
Теперь как работает запуск и остановка таймера через модель:
@override
void changeTimerState() {
if (_timeSubscription == null) {
print("subscribe");
_timer = timedCounter(oneSec, pomodoroSize);
_timerIsEnded.add(false);
_timerStateActive.add(true);
_timeSubscription = _timer.listen(_onTimeChange);
_timeSubscription.onDone(_handleTimerEnd);
} else {
if (_timeSubscription.isPaused) {
_timeSubscription.resume();
_timerStateActive.add(true);
} else {
_timeSubscription.pause();
_timerStateActive.add(false);
}
}
}
Чтобы таймер начал работать, надо на него подписаться _timeSubscription = _timer.listen(_onTimeChange);
. Остановить/продолжить реализуются через pause/resume подписки (_timeSubscription.pause();
/_timeSubscription.resume();
). Здесь же идет запись в поток состояния активности таймера _timerStateActive и поток информации о том, был включен таймер или нет _timerIsEnded.
Все контроллеры потоков требуют инициализации. Также стоит добавить начальные значения.
TimerViewModelImpl() {
_timerStateActive = new StreamController();
_timerStateActive.add(false);
_timerIsEnded = new StreamController();
_timeFormatted = new StreamController();
DateTime pomodoroTime = new DateTime.fromMicrosecondsSinceEpoch(pomodoroSize.inMicroseconds);
_timeFormatted.add(DateFormat.ms().format(pomodoroTime));
}
Получение потоков, как описано в интерфейсе:
@override
Stream<bool> get timeIsOver => _timerIsEnded.stream;
@override
Stream<bool> get timerIsActive {
return _timerStateActive.stream;
}
@override
Stream<String> get timeTillEndReadable => _timeFormatted.stream;
То есть, чтобы что-то написать в поток, нужен контроллер. Просто так взять и положить что-либо туда нельзя (исключение — когда поток генерируется в одной функции). А уже виджет забирает готовые потоки, управляют которыми контроллеры модели.
Теперь к виджету. ViewModel инициализируется в конструкторе состояния
_MyHomePageState() {
viewModel = new TimerViewModelImpl();
}
Затем в инициализации добавляются слушатели для потоков:
viewModel.timerIsActive.listen(_setIconForButton);
viewModel.timeIsOver.listen(informTimerFinished);
viewModel.timeTillEndReadable.listen(secondChanger);
Слушатели – это почти те же самые функции, которые были до этого, добавилась только проверка на null и немного поменялась _setIconForButton:
Icon iconTimerStart = new Icon(iconStart);
Icon iconTimerPause = new Icon(iconCancel);
void _setIconForButton(bool started) {
if (started != null) {
setState(() {
if (started) {
iconTimer = iconTimerPause;
} else {
iconTimer = iconTimerStart;
}
});
}
}
Остальные изменения в main.dart – это удаление всей логики таймера – теперь она живет во ViewModel.
Мой вариант реализации MVVM не использует дополнительных виджетов (таких как StreamBuilder), состав виджетов остался тем же. Ситуация аналогична тому, как в Android используется ViewModel и LiveData. То есть модель инициализируется, затем добавляются слушатели, которые уже реагируют на изменения модели.
Репозиторий проекта со всеми изменениями [4]
Автор: Yahhi
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/dart/296682
Ссылки в тексте:
[1] один пример: https://quickbirdstudios.com/blog/mvvm-in-flutter/
[2] Проект без MVVM: https://github.com/Yahhi/pomodoro_on_flutter/tree/2c3c123cec67b3ac4445740fafcedac2ebe49f1e
[3] вот этой статьи: https://www.dartlang.org/articles/libraries/creating-streams#honoring-the-pause-state
[4] Репозиторий проекта со всеми изменениями: https://github.com/Yahhi/pomodoro_on_flutter/tree/eaca1c9bad21f1e674584a8df57d21f89777eb06
[5] Источник: https://habr.com/post/427327/?utm_campaign=427327
Нажмите здесь для печати.