Технология Flutter быстро заняла почетную нишу на рынке разработки кроссплатформенных мобильных веб-приложений. Основное преимущество — единая база кода, которая позволяет одновременно разрабатывать приложения как на iOS, так и на Android, как для десктопных, так и для веб-приложений.
При этом технология помогает сэкономить самый драгоценный ресурс для каждого разработчика — время.
На сегодня Flutter Web уже вышел в стабильной версии, а это дает бóльшие гарантии как для разработчиков, так и для продукта. Обычно в продакшне мы не используем библиотеки и фреймворки в бете. А в случае с Flutter Web уже есть стабильная версия инструментария.
Flutter интуитивно понятен и довольно легок в освоении: и в этой статье я покажу это на практике. Мы выясним, как с его помощью всего за один день можно создать веб-приложение при уже имеющихся мобильных приложениях. Также выясним, какие подводные камни таит в себе Flutter Web и как их обойти без вреда для проекта.
С чего начать
Чтобы приступить к разработке веб-приложения на Flutter Web, нужно выполнить несколько команд:
flutter config --enable-web // добавление поддержки веба в файл настроек flutter create . // добавление поддержки веба в текущем проекте flutter build web // создание релизного билда
В этой статье детально разберем команду flutter build web
, отвечающую за создание папки web, которую затем можно разместить на веб-сервере. Это основная команда для создания проекта. Также выясним, что из себя представляет папка web.
Структура Flutter Web
Папка web, которая создается через команду flutter build web
, имеет следующий вид:
Давайте заострим внимание на трех ключевых файлах и поймем, за что они отвечают:
- index.html — простая html-страница, к которой подключаются другие файлы;
- flutter_service_worker.js — скрипт, который способен перехватывать и изменять команды по запросу ресурсов, а также выполнять кэширование данных;
- main.dart.js — сердце нашего веб-приложения, основной файл, ответственный за работу приложений на Flutter Web.
Генерацию main.dart.js можно увидеть на примере компиляции его кода с языка Dart в JavaScript:
Любое приложение, созданное на Flutter Web, обрабатывается при помощи движка Flutter Web Engine. Он содержит библиотеки и API, которые преобразовывают код в более низкоуровневый (для работы с html, CSS и Canvas). Компилятор Dart2js преобразовывает код с Dart в JavaScript.
Изначально Dart разрабатывался именно как язык веб-разработки, поэтому его преобразование на JS довольно хорошо оптимизировано.
Подводные камни Flutter Web
При разработке веб-приложения нужно понимать, что не все библиотеки и плагины, реализованные во Flutter, поддерживаются на Flutter Web. Поэтому важно искать альтернативы или писать собственные реализации, необходимые для корректной работы приложения.
Отсюда следует правило:
Если вы подозреваете, что ваше мобильное приложение на Flutter может обзавестись своей веб-версией, в разработке следует выбирать те плагины, которые поддерживает Flutter Web. Со списком поддерживаемых плагинов можно ознакомиться на сайте, отфильтровав поиск по категории Web.
Дополнительный список официальных плагинов для работы с Firebase также есть на сайте. Здесь можно увидеть, какие именно платформы поддерживают плагины: мобильные, web или desktop.
Где хранить данные и как реализовать сохранение на Flutter Web
Во Flutter Web существует четыре варианта, где можно хранить данные: Cookies, Local Storage, Session Storage и IndexedDB. Давайте сравним их и выделим сильные и слабые стороны каждого.
Сравнение вариантов для хранения данных
Cookies | Local Storage | Session Storage | IndexedDB | |
Тип данных |
Пары ключ-значение, строковые | Пары ключ-значение, строковые | Пары ключ-значение, строковые | Пары ключ-значение, разные типы данных |
Отправка вместе с запросом | Да | Нет | Нет | Нет |
Размер | 4 Кб | 5 Мб | 5 Мб | Зависит от браузера и свободного места на диске |
Время хранения | Expiry time / не ограничено | Не ограничено | До закрытия вкладки | Не ограничено |
Назначение |
Персонализации и отслеживания действий пользователя | Локальное хранилище небольших объемов данных между сессиями | Локальное хранилище небольших объемов данных в пределах одной вкладки | Локальное хранилище больших объемов данных между сессиями |
Давайте разберем подробнее:
- Cookies — небольшие фрагменты данных (весом 4 Кб), которые отправляются сервером и хранятся на устройстве веб-пользователя. По типу данных Cookies являются строковыми парами ключ-значение. Срок их хранения задается свойством expiry time или не ограничивается по времени.
- Local Storage — свойства глобального объекта браузера
window
. Данные здесь, как и в Сookies, хранятся в виде строковых пар ключ-значение, а время их хранения не ограничено. Для каждого домена браузер создает свой Local Storage. Доступ к данным в происходит гораздо быстрее, чем в Cookies. - Session Storage похож на Local Storage за исключением одного принципиального отличия — времени хранения данных. Session Storage существует только в рамках текущей вкладки браузера. Используется он намного реже, нежели Local Storage.
- IndexedDB представляет собой встроенную базу данных с индексами. Не имеет срока хранения, поэтому отлично подходит как локальное хранилище больших объемов данных между сессиями.
Сохранение данных на Flutter Web может быть реализовано такими способами:
- Ручная реализация — используя библиотеки
dart:html
,dart:js
,dart:indexed_db
. - Реализация при помощи готовых плагинов, поддерживаемых Flutter Web. Сегодня существует множество плагинов, которые охватывают типичные кейсы сохранения данных на вебе.
Отличия в сохранении данных на мобильной и веб-версиях Flutter
Разберем этот кейс на примере Shared Preferences. Наиболее подходящим способом хранения данных здесь является Local Storage. Сохранение легко реализовывается вручную посредством библиотеки dart:html
:
import ‘dart:html’; window.localStorage[‘selected_id’] = id;
Как видите, мы обращаемся к свойству localStorage
объекта window
и сохраняем туда необходимые нам значения. Сложность этого подхода в том, что нужно писать две различные версии кода — для мобильной и для веб-версии приложения.
Так было до лета 2020 года, пока на сайте не появился плагин Shared_Preferences. Он добавил поддержку веба и способность сохранять данные в Local Storage для браузера, в NSUserDefaults — для iOS, в Shared Preferences — для Android.
SharedPreferenced prefs = await SharedPreferences.getInstance(); prefs.setInt(“selected_id”, id);
Сохранение локальных баз данных во Flutter Web
В этом случае можно использовать Local Storage, IndexedDB или же прибегнуть к помощи библиотеки sql.js
. Но наиболее удобным выбором здесь станет использование готового ORM-решения. Сегодня одним из самых лучших ORM-плагинов является moor. Его функционал охватывает практически все типичные кейсы работы с базами данных, за исключением некоторых (пока еще здесь не реализована поддержка сущностей embedded).
Создание БД для мобильный и веб-версий происходит по-разному:
import ‘package:moor_flutter/moor_flutter.dart’; MyDatabase createDb() { return MyDatabase(FlutterQueryExecutor.inDatabaseFolder(path: ‘db.sqlite’)); } import ‘package:moor/moor_web.dart’; MyDatabase createDb() { return MyDatabase(WebDatabase(‘db’)); }
Используются разные импорты (moor_flutter
и moor_web
). Именно здесь и таится первый подводный камень — невозможно одновременно импортировать данные из разных библиотек. Для веб-приложения нельзя использовать мобильную версию и наоборот. Чем обусловлено такое ограничение и как его обойти?
Дело в том, что во Flutter Web невозможен импорт библиотеки dart:io
, которая используется для работы с файлами, сокетами и другими io-составляющими.
Но мы можем воспользоваться особой фичей языка Dart — условным импортом, который позволяет импортировать библиотеки при наличии заданного условия.
Пример условного импорта библиотеки выглядит так:
import ‘src/stub_impl.dart’ if (dart.library.io) ‘src/io_impl.dart’ if (dart.library.html) ‘src/web_impl.dart’ as some_lib;
Как видно из примера, импорт будет совершен при условии, что текущая версия приложения поддерживает библиотеки dart.library.io
или dart.library.html
соответственно.
Кэширование ресурсов в браузере и Assets
При работе на вебе часто применяют Assets. Они используются для обозначения файлов и объектов, необходимых для корректной работы веб-приложения.Во время использования веб-приложения пользователем, ресурсы будут кэшироваться в его браузере и при обновлении приложения могут отображаться некорректно. Это происходит, потому что в браузер загружаются варианты ресурсов из предыдущей версии приложения (до его обновления).
Ситуацию можно исправить несколькими способами:
- Cache-busting — основная идея в том, чтобы добавлять к URL-адресу ресурса информацию, связанную с версией приложения, хеша содержимого или же временными метками. Вы можете модифицировать название файла или добавлять новые параметры (через «?»), благодаря чему ресурс будет восприниматься как новый, и браузер загрузит его обновленную версию.
- Service worker — скрипт, который способен перехватывать запрос на ваши ресурсы и модифицировать их. В нем можно прописать стратегию, по которой будет реализовано кэширование.
На практике оба подхода применяются одинаково успешно.
Responsive UI
После сборки веб-приложения вы можете столкнуться с проблемой некорректного отображения интерфейса, который изначально разрабатывался для мобильных версий (неправильное масштабирование, разрывание текста). Чтобы избежать этого, приложение нужно сделать респонсивным — применить к нему дизайн, характерный для внешнего вида веба. При этом оставить мобильный вариант для мобильной версии.
Когда пользователь поворачивает девайс горизонтально или изменяет размер страницы в браузере, приложение должно корректно масштабироваться. Как отследить это?
Нужно самостоятельно проверить какое-либо условие (например, размер экрана), а затем — используя if и else, указать параметр, который нужно применить для выбранной ширины экрана:
@override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context) .size.width; return (screenWidth < 500) ? NarrowWidget() : WideWidget(); }
Но как узнать, какие начальные данные нужно указать для if и else? Можно прибегнуть к помощи MediaQuery — особому виджету, содержащему информацию о размере и ориентации экрана.
Еще один способ — использование константы kIsWeb. Метод покажет, было ли приложение скомпилировано для работы в вебе или нет. Таким образом можно понять, где именно открыто приложение — на мобильном устройстве или в браузере. Также можно использовать плагины platform_detect и web_browser_detect.
ResponsiveWidget и альтернативные варианты написания UI для веб-приложений
Чтобы постоянно не прописывать if и else в местах, где необходимо получить разный дизайн экрана, можно использовать ResponsiveWidget:
class MainScreen extends StatelessWidget { @override Widget build(BuildContext context) { return ResponsiveWidget.of(context).resolve( xLarge: (_) => MainWebScreen(), mobile: (_) => MainMobileScreen(), )(context); } }
Здесь виден основной экран приложения (MainScreen). В его методе build
используется ResponsiveWidget, который затем передал в метод resolve
варианты виджетов для разных типов экрана. В методе resolve
реализована логика получения информации об экране. Затем resolve
возвращает виджет, предназначенный для текущего размера экрана.
Существуют и альтернативные варианты создания UI для веб-приложений:
- Bootstrap-подход: экран условно разбивается на сетку, а элементы интерфейса размещаются в одну или несколько колонок в зависимости от ширины экрана. Скачать плагин можно по ссылке.
- Scaling-подход: элементы интерфейса увеличиваются или уменьшаются в зависимости от размера экрана. К этому варианту часто прибегают, когда дизайн для мобильного приложения и его веб-версии отличаются незначительно (размером или масштабируемостью элементов). Ознакомиться с плагином можно здесь.
Навигация во Flutter Web
Навигация во Flutter Web может быть реализована тремя способами:
- Использование вместо простых методов навигатора их Named-версий. Строчные параметры, которые вы туда передадите, и будут частью вашего URL после имени домена: Navigator.push -> Navigator.pushNamed.
- Использование плагинов, подходящих под ваши решения в реализации навигации. Здесь отметим fluro и flutter modular.
- Использование API: Navigator 2.0 — в нем содержится класс
RouteInformationParser
, который можно унаследовать и написать свой URL Parser для перехода на нужный скрин.
Описанные методы навигации на Flutter далеки от идеала, но они активно обновляются, так что советую следить за новинками и читать патчноуты.
Нереализованные свойства и недостатки
Среди распространенных «болячек», от которых до сих пор не излечился Flutter Web, часто выделяют следующие:
- отсутствует hot reload — реализован только hot restart;
- не все браузеры поддерживают стандартные шрифты;
- отсутствует поддержка SEO;
- довольно низкая производительность в мобильных браузерах.
Многие из этих фич планируется реализовать в ближайшее время, и, надеемся, к моменту выхода статьи этот список поредеет 🙂
Переход с Flutter на Flutter Web может показаться непривычным, но рассматривайте это как профессиональный челлендж. Тем более интерес, который IT-комьюнити проявляет к его развитию, подтверждает востребованность этой технологии в веб-разработке.
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: