Всем добрый денек! Надеюсь после первых трех статей, эта вам покажется не менее полезной.
Сегодня я постараюсь простым языком объяснить MVC паттерн.
И конечно же покажу все на практике!
Поехали!
Наш план
Часть 1 - введение в разработку, первое приложение, понятие состояния;
Часть 2 - файл pubspec.yaml и использование flutter в командной строке;
Часть 3 - BottomNavigationBar и Navigator;
Часть 4 (текущая статья) - MVC. Мы будем использовать именно этот паттерн, как один из самых простых;
Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;
Часть 6 - Работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;
Часть 7 - Создание своей темы, добавление кастомных шрифтов и анимации;
Часть 8 - Немного о тестировании;
Зачем MVC и прочие архитектурные принципы?
Возможно новичкам сначала совсем непонятно, для какой цели использовать архитектурные принципы, ведь без них хорошо.
Зачем все усложнять?
Наиболее веские причины:
Сложность кода - когда у вас небольшое приложение с одним или двумя экранами, будь это Flutter или нативное Android / iOS приложение, вы возможно спокойно обойдетесь без понимания принципов архитектуры. Другое дело, когда у проект приличных размеров, вы не сможете обойтесь без единых правил и принципов.
Сложность задачи - например: вам необходимо реализовать переключение между 3-мя, 5-ю или даже 10-ю темами (возможно задача не является распространенной). Без четкого понимания архитектуры это так не так просто сделать.
Сложность поддержки - если вы разрабатываете огромный коммерческий проект, скажем: Портал какого-либо города, объединенный с различными сервисами (карта, гостиницы и т.д.) вы по крайнее мере должны иметь команду. Каждый член команды должен действовать слаженно. А чтобы действовать слаженно нужно понимать чужой код. Без какого-либо единого подхода в вашей команде возникнет хаос и система потерпит крах.
Это наиболее распространенные причины по моему мнению
Также хорошая архитектура приложения наводит порядок в голове программиста :)
В чем суть MVC?
MVC (Model - View - Controller) является довольно старым изобретением и содержит три основных компонента:
Модель (Model) представляет собой данные, что и является сутью любого приложения. Само по себе приложение невозможно без данных. Вернемся к примеру из предыдущей главы: список поняшек. В том случае данными являлись пони, которые мы отображали в виде списка. Модель должна обратывать все, что с ней связано (сохранение и манипулирование данными). Ещё модель может иметь отношения (один к одному, один ко многих, многие ко многим). Практически, модель - это класс Dart, например: Pony
Представление (View), в нашем случае это Flutter виджеты (кнопки, текст, списки), которые будут отображать нашу модель. View должно знать о модели и о её свойствах. Пользователь взаимодействует только с представлением и инициирует различные события (нажатие кнопки, свайп пальцем и т.д.). События могут оказывать влияние на модель, это происходит не напрямую, а через контроллер. Практически, представление - это виджеты: Text, Scaffold, AppBar, ListView и другие.
Контроллер (Controller) получает необработанные данные (например от сервера) и заполняет ими модель. При возникновении какого-либо события контроллер может изменить модель. После этого измененная модель снова отобразиться в представлении. Практически, это специальный класс, который мы вынесем отдельно, например: HomeController
Более подробная информация есть на Википедии.
MVC на деле
Ну что ж применим полученные знания на практике.
Для Flutter есть специальный pub-пакет, который мы уже подключили во части II в pubspec.yaml
файле:
# блок зависимостей
dependencies:
flutter:
sdk: flutter
# подключение необходимых pub-пакетов
# используется для произвольного размещения
# компонентов в виде сетки
flutter_staggered_grid_view: ^0.4.0
# этот пакет содержит вспомогательные
# элементы для реализации MVC паттерна
# в Flutter приложении
mvc_pattern: ^7.0.0
# большая часть данных будет браться из сети,
# поэтому мы будем использовать http для
# осуществления наших запросов
http: ^0.13.3
После этого выполним pub get
команду в корне нашего проекта:
flutter pub get
Также вы можете воспользоваться встроенными возможностями Android Studio (блок Flutter commands):
Воспользуемся готовым кодом из прошлых частей и сделаем нашу домашнюю страницу в соотвествии с паттерном MVC.
Мы уже имеем модель:
И представление:
Обратите внимание, что представление (View) содержит лишнюю логику, которая должна быть вынесена в контроллер.
Не поленимся и вынесем)
Для этого создадим новую папку controllers
и в ней файл home_controller.dart
:
import 'package:flutter/material.dart';
import 'package:mvc_pattern/mvc_pattern.dart';
import '../models/tab.dart';
// библиотека mvc_pattern предлагает
// нам специальный класс ControllerMVC,
// который предоставит нам setState метод
class HomeController extends ControllerMVC {
// ссылка на объект самого контроллера
static HomeController _this;
static HomeController get controller => _this;
// сам по себе factory конструктор не создает
// экземляра класса HomeController
// и используется для различных кастомных вещей
// в данном случае мы реализуем паттерн Singleton
// то есть будет существовать единственный экземпляр
// класса HomeController
factory HomeController() {
if (_this == null) _this = HomeController._();
return _this;
}
HomeController._();
// GlobalKey будет хранить уникальный ключ,
// по которому мы сможем получить доступ
// к виджетам, которые уже находяться в иерархии
// NavigatorState - состояние Navigator виджета
// знак _ как уже было отмечено указывает на то,
// что это private переменная, поэтому мы
// не сможем получить доступ извне к _navigatorKeys
final _navigatorKeys = {
TabItem.POSTS: GlobalKey<NavigatorState>(),
TabItem.ALBUMS: GlobalKey<NavigatorState>(),
TabItem.TODOS: GlobalKey<NavigatorState>(),
};
// ключевое слово get указывает на getter
// мы сможем только получить значение _navigatorKeys,
// но не сможем его изменить
// это называется инкапсуляцией данных (один из принципов ООП)
Map<TabItem, GlobalKey> get navigatorKeys => _navigatorKeys;
// текущий выбранный элемент
var _currentTab = TabItem.POSTS;
// то же самое и для текущего выбранного пункта меню
TabItem get currentTab => _currentTab;
// выбор элемента меню
// здесь мы делаем функцию selectTab публичной
// чтобы иметь доступ к ней из HomePage
// обратите внимание, что библиотека mvc_pattern
// предоставляет нам возможность вызывать setState
// в контроллере, что очень удобно
void selectTab(TabItem tabItem) {
setState(() => _currentTab = tabItem);
}
}
Большую часть кода мы вынесли из HomePage.dart
Теперь нам осталось подключить наш контроллер к нашему представлению:
import 'package:flutter/material.dart';
import 'package:mvc_pattern/mvc_pattern.dart';
import '../../models/tab.dart';
import '../../controllers/home_controller.dart';
import 'bottom_navigation.dart';
import 'tab_navigator.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
// наше состояние теперь расширяет специальный класс
// StateMVC из пакета mvc_pattern
class _HomePageState extends StateMVC {
// ссылка на наш контроллер
HomeController _con;
// super вызывает конструктор StateMVC и
// передает ему наш контроллер
_HomePageState() : super(HomeController()) {
// получаем ссылку на наш контроллер
_con = HomeController.controller;
}
// здесь почти ничего не изменилось
// только currentTab и selectTab теперь
// являются частью нашего контроллера
@override
Widget build(BuildContext context) {
// WillPopScope переопределяет поведения
// нажатия кнопки Back
return WillPopScope(
// логика обработки кнопки back может быть разной
// здесь реализована следующая логика:
// когда мы находимся на первом пункте меню (посты)
// и нажимаем кнопку Back, то сразу выходим из приложения
// в противном случае выбранный элемент меню переключается
// на предыдущий: c заданий на альбомы, с альбомов на посты,
// и после этого только выходим из приложения
onWillPop: () async {
if (_con.currentTab != TabItem.POSTS) {
if (_con.currentTab == TabItem.TODOS) {
_con.selectTab(TabItem.ALBUMS);
} else {
_con.selectTab(TabItem.POSTS);
}
return false;
} else {
return true;
}
},
child: Scaffold(
// Stack размещает один элемент над другим
// Проще говоря, каждый экран будет находится
// поверх другого, мы будем только переключаться между ними
body: Stack(children: <Widget>[
_buildOffstageNavigator(TabItem.POSTS),
_buildOffstageNavigator(TabItem.ALBUMS),
_buildOffstageNavigator(TabItem.TODOS),
]),
// MyBottomNavigation мы создадим позже
bottomNavigationBar: MyBottomNavigation(
currentTab: _con.currentTab,
onSelectTab: _con.selectTab,
),
),);
}
// Создание одного из экранов - посты, альбомы или задания
Widget _buildOffstageNavigator(TabItem tabItem) {
return Offstage(
// Offstage работает следующим образом:
// если это не текущий выбранный элемент
// в нижнем меню, то мы его скрываем
offstage: _con.currentTab != tabItem,
// TabNavigator мы создадим позже
child: TabNavigator(
navigatorKey: _con.navigatorKeys[tabItem],
tabItem: tabItem,
),
);
}
}
В представлении практически ничего не изменилось, мы только вынесли основную логику из HomePage в наш HomeController
.
Вуаля! Все работает как прежде.
Немного слов об архитектуре Flutter приложений
Flutter является декларативным фреймворком и поэтому архитектура Flutter приложения всегда сводится к управлению состоянием StatefulWidget
'ов.
Существует множество подходов по управлению состоянием.
Более подробно об этом написано в самой документации по Flutter
Заключение
Поздравляю вас.
Хотелось бы отметить, что на этом знания об MVC не исчерпываются.
К тому же мы ещё не раз будет создавать новые контроллеры и модели.
Так что все впереди! До скорой встречи.
Полезные ссылки:
DonAlPAtino
Вы бы только писали код сразу в null safety. Для начинающие же, половина и так не понятно, а оно еще и не компилируется :-(.
Расскажите как контроллер в null safety переписать, пожалуйста? «The non-nullable variable '_this' must be initialized.» Остальное уговорил, а это никак. Хотя наверняка стандартные примеры быть должны где-то…
KiberneticWorm Автор
Все довольно просто. Вы могли использовать командную строку для миграции в null safety:
Затем переходите по ссылки в браузере и нажимаете APPLY MIGRATES.
Я только что добавил null-safety, теперь вы также можете воспользоваться исходным кодом на GIthub
mrxten
Не советую так делать на реальном проекте. Бывает бизнес логика, завязанная на null.и миграцией ее не разрешить
KiberneticWorm Автор
Да такое часто бывает. В таких ситуациях лучше вручную внести изменения