Всем добрый денек! Надеюсь после первых трех статей, эта вам покажется не менее полезной.
Сегодня я постараюсь простым языком объяснить 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 не исчерпываются.
К тому же мы ещё не раз будет создавать новые контроллеры и модели.
Так что все впереди! До скорой встречи :)
Полезные ссылки:
mvc_pattern
MVC Overview
Подходы для управления состоянием
Исходный код Github