Доброго времени суток, дорогие читатели! Меня зовут Сурен, и я разработчик.
Поскольку моя предыдущая статья о том, как бекендер в мобильную кроссплатформу лез, не утонула в минусах, я решил продолжить делиться своим опытом познания данной замечательной технологии =)
Написано немало статей про MVVM, его реализацию на различных технологиях и на Flutter, в частности. Но мне они давались с трудом, и не было понимания, как оно в итоге работает. Возможно, сказывается особенность восприятия “Бекендера” =) Поэтому, если среди читателей есть люди с похожим складом ума, возможно эта статья поможет и Вам понять, что такое MVVM и как его реализовать на Flutter простым способом.
Для начала немного википедии.
MVVM (Model-View-ViewModel) — шаблон проектирования архитектуры приложения. Пришел на смену шаблону MVC|MVP (Model-View-Controller/Presenter). Актуален для платформ, в которых присутствует концепция «связывания данных», позволяющая связывать данные с визуальными элементами в обе стороны. На схеме это выглядит так:

Где
-
Model представляет из себя слой бизнес-логики, который включает описание фундаментальных данных, необходимых для работы приложения, и логику работы с ними.
-
View является графическим интерфейсом. Подписан на изменения ViewModel, а при действиях с интерфейсом вызывает команды ViewModel для изменения данных в Модели.
-
ViewModel же с одной стороны — абстракция Представления, а с другой — обёртка данных из Модели, подлежащих связыванию. То есть она содержит Модель, преобразованную к Представлению, а также команды, которыми может пользоваться Представление, чтобы влиять на Модель.
Перейдем к практике.
Для обеспечения “связывания данных” я использую связку Provider+ChangeNotifier. Provider представляет собой смесь Инъекции Зависимостей (Dependency Injection или DI) и управления состоянием, а ChangeNotifier позволяет отслеживать изменения ViewModel и перестраивать наш UI.
Построим простейший ToDoList на данном подходе и попутно будем разбираться, как он работает.
Внимание! В качестве примера для наглядности и удобства я буду реализовывать каждый виджет в одном файле, то есть и ViewModel, и View(Виджет) и его Model. Но на боевых проектах, особенно если вы работаете в команде, эти сущности нужно разносить по разным файлам.
Для начала создадим новый проект приложения на flutter. Перед нами откроется чистый проект, представляющий из себя приложение счетчик или кликер.

В main.dart у нас реализован основной метод main(), запускающий приложение MyApp, которое состоит из 1 StatefullWidget — MyHomePage, реализующего счетчик нажатий. Приложение можно запустить и проверить, что все работает.

Но мы собрались делать ToDoList, следовательно пока оставляем main.dart. Создаем рядом новый файл todo_list_widget.dart, а в нем классы:
-
_ViewModel, реализующий ChangeNotifier,
-
ToDoListWidget, реализующий StatelessWidget,
-
_ModelState, в котором мы будем хранить состояние нашей модели.
todo_list_widget.dart
class _ModelState {} class _ViewModel extends ChangeNotifier {} class ToDoListWidget extends StatelessWidget { const ToDoListWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { var _viewModel = context.watch<_ViewModel>(); return Container(); } }
В Dart символ “_” перед именем свойства/класса/метода делает его приватным (доступным только в пределах текущего файла .dart).
Добавим файл todo_item.dart и реализуем в нем класс Модели ToDoItem. Цель — сделать некий список дел, так что свойства нужные нам в данном классе:
-
name — наименование задачи (по совместительству ключ),
-
done — флаг о выполнении.
Также, чтобы у нас была возможность “редактировать” элементы списка, добавим метод copyWith. Наш класс модели будет выглядеть следующим образом:
Класс ToDoItem
class ToDoItem { final String name; final bool done; ToDoItem({ required this.name, this.done = false, }); ToDoItem copyWith({ String? name, bool? done, }) { return ToDoItem( name: name ?? this.name, done: done ?? this.done, ); } }
Возвращаемся к основному.
В стейте мы будем хранить список элементов ToDo листа. Для обеспечения иммутабельности список у нас будет “final”, а для редактирования мы будем его копировать, поэтому добавим метод “copyWith”. В нашем случае отдельный стейт выглядит как “бойлерплейт“, но для наглядности и последующего масштабирования он полезен.
После этих манипуляций класс принял следующий вид:
todo_list_widget.dart
class _ModelState { final List<ToDoItem> items; _ModelState({ this.items = const <ToDoItem>[], }); _ModelState copyWith({ List<ToDoItem>? items, }) { return _ModelState( items: items ?? this.items, ); } }
Переходим к ViewModel, поскольку задача данного класса — быть прослойкой между интерфейсом и данными. С одной стороны мы должны в нем следить за изменениями модели и дергать перестраивание интерфейса, а с другой — должны иметь методы воздействия на эту модель. Добавим в него private свойство “_state” и дополним публичным сеттером и геттером. Геттер возвращает приватный стейт, а сеттер заменяет его новым значением и уведомляет прослушивающие виджеты при изменении стейта.
_ModelState
var _state = _ModelState(); _ModelState get state => _state; set state(_ModelState val) { _state = _state.copyWith(items: val.items); notifyListeners(); }
“notifyListeners” — метод в классе “ChangeNotifier”, уведомляющий виджетов-слушателей об изменении модели и дающий команду перестроиться.
Для реализации ToDo списка нам нужны следующие методы:
Добавления элемента в список
addItem(ToDoItem item) { var list = state.items.toList(); if (list.any((element) => element.name == item.name)) { throw Error(); } else { list.add(item); state = state.copyWith(items: list); } }
Удаления элемента из списка
dellItem(ToDoItem item) { var list = state.items.toList(); list.removeWhere((element) => element.name == item.name); state = state.copyWith(items: list); }
Переключения состояния элемента списка
toogleItem(ToDoItem item) { var list = state.items.toList(); var index = list.indexWhere((element) => element.name == item.name); list[index] = item.copyWith(done: !item.done); state = state.copyWith(items: list); }
После каждого действия мы вызываем Сеттер стейта для того, чтобы уведомить об изменении слушателей и перестроить стейт.
На первый взгляд основная логика готова, так что перейдем к View (виджету).
Пришло время добавить в проект пакет Provider. Для этого можно либо отредактировать файл pubspec.yaml, добавив в него зависимость от пакета Provider, либо выполнив команду в консоли:
flutter pub add provider — данная команда, собственно, и добавит зависимость в тот же файл и скачает пакет последней версии.
Чтобы наш виджет мог реагировать на изменение стейта, для начала обернем его в ChangeNotifierProvider. В метод создания мы передаем нашу ViewModel, а в качестве ребенка — наш Виджет. Для таких оберток удобно делать статический метод create внутри виджета, который и инициирует ViewModel и передает ее в виджет ребенка. Теперь для получения доступа к ViewModel внутри билдера виджета, мы можем ее получить через context. Для этого мы и используем пакет Provider.
Виджет ToDoListWidget
class ToDoListWidget extends StatelessWidget { const ToDoListWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { var _viewModel = context.watch<_ViewModel>(); return Container(); } static Widget create() => ChangeNotifierProvider( create: (_) => _ViewModel(), child: const ToDoListWidget(), ); }
Метод watch<ClassName>() не только отдает нам через провайдер экземпляр модели, которая была инициализирована выше по дереву виджетов, но и добавляет текущий виджет в качестве слушателя изменений в модели. То есть, когда мы в модели вызываем метод notifyListeners, наш виджет-слушатель перестраивается, за счет чего и достигается “связывание данных” между моделью и представлением.
Теперь набросаем простенький интерфейс, аналогичный базовому виджету MyHomePage, но с той лишь разницей, что в качестве тела виджета у нас будет список. В качестве списка будем использовать ListView.builder, поскольку он рендерит только те элементы, которые видны на экране, и в целом удобен.
ToDoListWidget.build
@override Widget build(BuildContext context) { var _viewModel = context.watch<_ViewModel>(); return Scaffold( appBar: AppBar( title: const Text("ToDo List"), ), body: ListView.builder( itemBuilder: (_, int index) => ToDoItemWidget( item: _viewModel.state.items[index], onDelete: _viewModel.dellItem, onToogle: _viewModel.toogleItem, ), itemCount: _viewModel.state.items.length, ), floatingActionButton: FloatingActionButton( onPressed: () { showDialog( context: context, builder: (_) => AddTaskDialog(onFinish: _viewModel.addItem), ); }, tooltip: 'Add task', child: const Icon(Icons.add), ), ); }
В качестве элемента списка напишем виджет ToDoItemWidget, который отдаст нам CheckboxListTile (элемент списка с чекбоксом), обернутый в Dismissible (для реализации удаления свайпом), и будет пробрасывать 2 делегата: на удаление элемента из списка и на изменение состояния элемента списка (выполнено/не выполнено).
Виджет ToDoItemWidget
class ToDoItemWidget extends StatelessWidget { final ToDoItem item; final Function(ToDoItem) onDelete; final Function(ToDoItem) onToogle; const ToDoItemWidget({ Key? key, required this.item, required this.onDelete, required this.onToogle, }) : super(key: key); @override Widget build(BuildContext context) { return Dismissible( background: Container(color: Colors.red), key: Key(item.name), onDismissed: (_) { onDelete(item); }, child: CheckboxListTile( value: item.done, onChanged: (_) { onToogle(item); }, title: Text(item.name), )); } }
Для добавления элемента нам нужно сначала где-то взять название для задачи. Для этого напишем виджет AddTaskDialog. Он представляет из себя AlertDialog с текстовым полем и делегатом, который в случае успеха возвращает нам новенькую задачу, а также сообщает об ошибках через всплывающие уведомления.
AddTaskDialog
class AddTaskDialog extends StatelessWidget { final ValueChanged<ToDoItem> onFinish; const AddTaskDialog({ Key? key, required this.onFinish, }) : super(key: key); @override Widget build(BuildContext context) { String? text; return AlertDialog( title: const Text("Add new Task"), content: TextField( autofocus: true, onChanged: (String _text) { text = _text; }), actions: <Widget>[ TextButton( child: const Text("Add"), onPressed: () { if (text != null) { try { onFinish(ToDoItem(name: text!)); Navigator.of(context).pop(); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Task ${text!} exist!"))); } } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Enter Task name"))); } }, ), TextButton( child: const Text("Close"), onPressed: () { Navigator.of(context).pop(); }, ), ], ); } }
Всё. Теперь можно собрать и посмотреть, что он делает =)

Все заложенные нами кейсы работают =)
В дальнейшем можно прикрутить sqlite, добавить инициализацию и синхронизацию, и вот — простенький задачник готов (шутка).
Я специально не затронул в данной статье редактирование задач. Если захотите попробовать повторить, Вы сможете это сделать сами =)
Ссылка на репозиторий с данным примером.
В сухом остатке.
Я не утверждаю, что MVVM — это лучший шаблон проектирования для мобильных приложений на Flutter. Это пока первое, что я осознал и принял на вооружение после statefullWidget. Самое главное для меня — это его масштабируемость и применимость для достаточно больших приложений. Плюс он отвечает требованиям Чистой архитектуры. Только прошу, не рассматривайте мой пример как Архитектурный. Я показал в коде на примере приложения для чек-листа, как реализовать данный шаблон.
ссылка на оригинал статьи https://habr.com/ru/company/digdes/blog/660411/
Добавить комментарий