- 1. Введение
- 2. Backend
- 2.1. Инфраструктура.
- 2.2. Доменное имя. SSL
- 2.3. Серверное приложение на Дарт
- ...
- 3. Web
- 3.1. FlutterWeb страница (мы находимся здесь)
- ...
- 4. Mobile
- ...
Подготовка
В прошлый раз мы закончили на том, что наш веб-сервер получил доменное имя и научился устанавливать безопасное соединение с клиентом. Однако нам пока совсем нечего показать нашему будущему пользователю. Хотя мы уже можем поделиться идеей стартапа и сообщить дату релиза MVP. Для такой задачи подойдёт информационная web-страница. Напишем её на Dart с использованием фреймворка FlutterWeb. Все наши клиентские приложения сервиса станут расширением именно этой страницы. Постараемся вести разработку максимально адаптивно и структурировано, чтобы развитие и сборки под нужные платформы (web-android-iOS) стали просто рутиной.

Начнём с установки Flutter:
- Установим git
- Склонируем репозиторий с beta версией Flutter командой
git clone https://github.com/flutter/flutter.git -b beta
- Для запуска команд flutter из командной строки необходимо указать в операционной системе путь до его исполняемых файлов. Откроем переменные ОС, для этого начнём вводить «изменение переменных среды текущего пользователя» в строке поиска
В окне выберем переменную Path и нажмём Изменить. В открывшемся списке создадим новую строку с адресом до исполняемых файлов flutter в файловой системе, например C:\flutter\bin - Установим расширение VScode для flutter
- Перезапустим VScode (чтобы применились новые переменные ОС) и в терминале проверим состояние flutter командой
flutter doctor
здесь самое важное, что flutter установлен в beta версии (с поддержкой web разработки) - Теперь активируем веб разработку командой
flutter config --enable-web
Создание нового проекта и запуск отладки
Создаём новый проект командой
flutter create <название проекта>
Сразу откроем его в VScode командой
code <название проекта>
Откроем файл main.dart в папке lib и запустим отладку командой F5:

Возможно при первом запуске отладки понадобится выбрать Chrome устройством для отладки:

Удалим содержимое файла main.dart. Добавим пустой метод main и корневой класс приложения, возвращающий в методе build() экземпляр MaterialApp:

Далее создадим следующий набор вложенных папок проекта:

Кратко опишем назначение каждой из них:
- di — механизм для связи между компонентами приложения. Здесь будут создаваться и регистрироваться все необходимые сервисы, репозитории, сетевые клиенты, необходимые для работы приложения. Я буду использовать библиотеку GetIt
- domain — data-объекты. Классы представления данных
- res — цвета, строки, импорты путей к картинкам и шрифтам. Всё, что связано со статическими ресурсами
- service — сервисы для работы с данными
- ui — интерфейс
- utils — вспомогательные классы
В файле pubspec.yaml добавим необходимые зависимости:

Подготовка к масштабированию элементов UI
Предполагается, что наша страница должна адаптироваться в зависимости от размеров экрана клиентского устройства и его расположения (портретный или ландшафтный режим).
Начнём с картинок заднего фона. Их подготовка не входит в тему статьи, поэтому просто оставлю здесь эти две ссылки:
- Pixabay.com — хранилище контентных фотографий
- Paint.net — графический редактор
Готовые картинки разместим в папке /assets/images, в файле pubspec.yaml добавим этот путь к ресурсам:

Я предпочитаю доступ к ресурсам в виде дерева c параметрами. В данном случае путь к картинке заднего фона заглушки:
images.background(bool isPortrait).stub
Для этого в папке res создадим файл images.dart с классами адресов картинок:

Для масштабирования размеров интерфейса и шрифтов мы подключили библиотеку ScreenUtil. Её функциональность сводится к двум вещам:
- Регистрация «базового» размера экрана. Здесь необходимо задать ширину и высоту экрана, для которого ведётся основная верстка и необходимость масштабирования шрифтов.
- Набор расширений, позволяющий для чисел (num) применить масштабирующий коэффициент. Например 100.w означает, что результатом этого выражения будет для экрана шириной 1920dp => 100dp, а для экрана iPhone8 с шириной 414dp => 100х(414/1920) = 21,6dp. То есть в пять раз меньше. Также предусмотрены расширения для параметра высоты и размера шрифтов.
Создадим файл /utils/screen_util_ext.dart и статический метод инициализации в нём:

Вызов метода инициализации масштабирования добавим в метод build() корневого виджета:

Расширим функциональность библиотеки масштабирования несколькими дополнительными расширениями в файле /utils/screen_util_ext.dart:



Инъекция зависимостей
Пришло время внедрить механизм создания и регистрации компонентов приложения с помощью библиотеки GetIt. В папке lib/DI/ создадим файл di_container.dart. В нём напишем статический метод getItInit() и инициализируем экземпляр контейнера GetIt. Зарегистрируем первый компонент — экземпляр класса Images:

Вызов метода инициализации добавим в main():

Доступ к компоненту Images будет выглядеть так:

Таким же образом зарегистрируем класс с ресурсами строками.
Страница-заглушка
Теперь в папке UI создадим файл stub.dart с классом страницы заглушки StubScreen, расширим базовый класс StatelessWidget и переопределим его абстрактный метод build(). Наша страница представляет собой картинку на заднем плане и два информационных блока перед ней, размещающихся в зависимости от ориентации экрана.



Репозитории и сервис
Для динамического отображения оставшегося до релиза времени необходимо:
- Получить с сервера настройки с датами начала разработки и релиза
- Создать поток событий изменения оставшегося времени
- Объединить эти данные, передав в выходной поток для отображения на UI
Опишем доменные объекты (POJO) для этих данных:


Репозитории для получения настроек и создания потока событий:


Сервис для логики событий:

Зарегистрируем эти компоненты в DI контейнере:

Виджет оставшегося времени
Оставшееся до релиза время можно представить как 4 числа: дни, часы, минуты, секунды. Представим эти параметры в виде перечисления:

Добавим функциональности параметрам с помощью расширения:

Виджет для отображения круговой шкалы, числа и подписи будет анимированным, для этого расширим класс StatefulWidget. Его особенность в том, что Element (построенное и отображаемое представление) соотносится не с самим виджетом, а с его состоянием (State). Состояние, в отличие от виджета мутабельно. То есть его поля могут быть изменены без полного пересоздания экземпляра.


Здесь необходимо уточнить, что такое Animation, AnimationController и TickerProviderStateMixin. Итак AnimationController — обёртка над простым параметром double value. Значение этого параметра меняется линейно в пределах от 0,0 до 1,0 (также его можно менять в обратную сторону или сбрасывать в 0,0). Однако для изменения этого параметра используется специальный объект TickerProviderStateMixin, который является обязательным параметром для AnimationController и сообщает ему, что графический движок готов построить новый кадр. Получив такой сигнал AnimationController рассчитывает сколько времени прошло от предыдущего кадра и вычисляет насколько нужно изменить значение своего value. Объекты Animation подписываются на AnimationController и содержат в себе некоторую функцию зависимости выходного значения от линейного (по времени) изменения значения AnimationController.
Метод инициализации состояния initState() вызывается один раз при создании:

При уничтожении состояния виджета вызывается метод dispose():


Представлением виджета будет стек (Stack), с помещёнными в него AnimatedBuilder для числа и шкалы:

Остаётся реализовать графический примитив в виде дуги:

Добавим 4 таких виджета на экран заглушки:

Сборка и релиз
Перед сборкой приложения необходимо заменить название и описание приложения в файлах ./web/index.html ./web/manifest.json и pubspec.yaml.
Останавливаем отладку и собираем релиз приложения командой
flutter build web
Готовое приложение находится в директории ./build/web/. Обратите внимание, что файлы .last_build_id и main.dart.js.map служебные и могут быть удалены.
Разместим приложение на сервере, подготовленном в предыдущей статье. Для этого достаточно скопировать содержимое директории ./build/web/ в /public/ нашего сервера:
scp -r ./* root@91.230.60.120:/public/
Результат
Исходный код github
Вопросы и комментарии приветствуются. Пообщаться с автором можно в Telegram канале.
Вместо заключения
Наше клиентское приложение уже готово получить первые данные с сервера — сведения о дате релиза. Для этого в четвертой статье мы создадим каркас серверного приложения и разместим его на сервере.
gudvinr
К третьей части из серии статей про веб-сервис на языке Dart наконец-то появился Dart, осталось совсем немного и увидим сервис.