Всем добрый денек! Надеюсь после первых трех статей, эта вам покажется не менее полезной.

Сегодня я постараюсь простым языком объяснить 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) является довольно старым изобретением и содержит три основных компонента:

  1. Модель (Model) представляет собой данные, что и является сутью любого приложения. Само по себе приложение невозможно без данных. Вернемся к примеру из предыдущей главы: список поняшек. В том случае данными являлись пони, которые мы отображали в виде списка. Модель должна обратывать все, что с ней связано (сохранение и манипулирование данными). Ещё модель может иметь отношения (один к одному, один ко многих, многие ко многим). Практически, модель - это класс Dart, например: Pony

  2. Представление (View), в нашем случае это Flutter виджеты (кнопки, текст, списки), которые будут отображать нашу модель. View должно знать о модели и о её свойствах. Пользователь взаимодействует только с представлением и инициирует различные события (нажатие кнопки, свайп пальцем и т.д.). События могут оказывать влияние на модель, это происходит не напрямую, а через контроллер. Практически, представление - это виджеты: Text, Scaffold, AppBar, ListView и другие.

  3. Контроллер (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 не исчерпываются.

К тому же мы ещё не раз будет создавать новые контроллеры и модели.

Так что все впереди! До скорой встречи.

Полезные ссылки: