Реализация ViewModel во Flutter

Что такое ViewModel

ViewModel — одна из трех составляющих шаблона проектирования архитектуры Model-View-ViewModel (MVVM). Согласно данной концепции, модель содержит логику работы с данными и их описание, представление (View) является графическим интерфейсом, а ViewModel связывает их.


В рамках НИР реализуется ViewModel для мобильного приложения, позволяющего искать и отображать рецепты. 

Для чего нужна ViewModel

  1. Разделение бизнес-логики и интерфейса.
  2. Кеширование данных.
  3. Взаимодействие между несколькими экранами.
Что содержит в себе ViewModel
  1. Данные из запроса.
  2. Методы для запроса данных из сети.
  3. Список текущих объектов в обработке.
  4. Индикатор загрузки.
  5. Методы для отправки команд интерфейсу.
Как реализуется модель представления

Для удобства и возможности добавлять новые модели создан абстрактный класс BaseViewModel<T, E>, расширяющий ChangeNotifier. Он может уведомлять другие классы об изменениях (например, чтобы те перестроили UI). Базовая ViewModel содержит данные в виде типизированного (от Т) списка, а также методы для работы с ним: добавление, удаление, замена и так далее. Каждый метод имеет две вариации: с уведомление подписчиков и без него. 
Кроме того, имеется переменная-флаг, отвечающая за состояние загрузки, и метод по заданию её значения и уведомления подписчиков.
Реализован механизм оповещения класса View о конкретном событии с помощью uiEventSubject. Эта переменная инициализируется с помощью PublishSubject<E>(), в которую ViewModel будет посылать события типа Е. Для того чтобы UI класс мог реагировать на эти события, определим специальный метод startUIListening:
final _uiEventSubscription = CompositeSubscription();
StreamSubscription startUIListening(Function(E) listener)
    => _uiEventSubscription.add(uiEventSubject.listen(listener));
Далее опишем реализацию конкретной модели представления для экрана со списком рецептов. Данный класс будет наследоваться от базовой ViewModel. Основной метод загрузки рецептов принимает строку с ключевыми словами и вызывает метод репозитория для выполнения запроса к серверу, а затем добавляет результат в хранилище данных. При этом запрос в сеть обрамляется изменением состояния загрузки, чтобы на интерфейсе отобразить индикацию загрузки. Ниже приведена общая схема основного метода ViewModel, скачивающего данные [1].
Future<void> loadData({String value, bool showLoading = true}) async {
setLoading(value: true, notify: showLoading);
final result = await dataRepository.getData(
search: value,
);
if (result.result) {
silenceClearItems();
silenceAddRange(result.value as List<Data>);
}
setLoading(value: false);
}
Здесь методы методы silenceClearItems и silenceAddRange имеют приставку, означающую, что они не уведомляют подписчиков о том, что данные изменились. Это делается для того, чтобы предупредить слишком частое обновление UI. Метод notifyListeners вызовется в заключительном setLoading, унаследованном от базового класса.  
void setLoading({bool value, bool notify = true}) {
loading = value;
if (notify) {
notifyListeners();
}
}
Кроме того, в модели представления содержится метод, который класс View вызывает при нажатии на карточку конкретного рецепта, чтобы открыть полную информацию. Для этого выполняются следующие шаги:
  1. Занесение id рецепта в processingIds, чтобы предупредить многократные клики.
  2. Возведение индикатора загрузки.
  3. Запрос в сеть.
  4. Снятие индикатора загрузки.
  5. Удаление id из processingIds.
  6. Отправка события в uiEventSubject.
Взаимодействие View с ViewModel
Класс View подписывается на изменения во ViewModel с помощью consumer из пакета Provider [2]. Это позволяет виджету обновляться всякий раз, когда модель представления шлет notifyListeners. В структуре дерева класса View добавляется проверка, по которой отображается индикатор загрузки при соответствующем значении в модели представления.

Кроме того, View подписывается на события UI, которые модель представления отправляет в uiEventSubject [2]. В нашем случае по этому событию будет открываться экран с полной информацией о рецептах. 

Заключение

Таким образом, реализован базовый класс ViewModel и конкретная модель представления для рецептов. Данное архитектурное решение позволяет структурировать код и сделать его легко расширяемым. В дальнейшем работа может быть дополнена кешированием пользовательских запросов. 

Источники

[1] Ogubuike R., Adib A., Orji R. Masa: AI-Adaptive Mobile App for Sustainable Agriculture //2021 IEEE 12th Annual Information Technology, Electronics and Mobile Communication Conference (IEMCON). – IEEE, 2021. – С. 1064-1069.
[2] Haider A. Evaluation of cross-platform technology Flutter from the user’s perspective. – 2021.