Технология 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.
Папка web, которая создается через команду flutter build web
, имеет следующий вид:
Давайте заострим внимание на трех ключевых файлах и поймем, за что они отвечают:
Генерацию main.dart.js можно увидеть на примере компиляции его кода с языка Dart в JavaScript:
Любое приложение, созданное на Flutter Web, обрабатывается при помощи движка Flutter Web Engine. Он содержит библиотеки и API, которые преобразовывают код в более низкоуровневый (для работы с html, CSS и Canvas). Компилятор Dart2js преобразовывает код с Dart в JavaScript.
Изначально Dart разрабатывался именно как язык веб-разработки, поэтому его преобразование на JS довольно хорошо оптимизировано.
При разработке веб-приложения нужно понимать, что не все библиотеки и плагины, реализованные во Flutter, поддерживаются на Flutter Web. Поэтому важно искать альтернативы или писать собственные реализации, необходимые для корректной работы приложения.
Отсюда следует правило:
Если вы подозреваете, что ваше мобильное приложение на Flutter может обзавестись своей веб-версией, в разработке следует выбирать те плагины, которые поддерживает Flutter Web. Со списком поддерживаемых плагинов можно ознакомиться на сайте, отфильтровав поиск по категории Web.
Дополнительный список официальных плагинов для работы с Firebase также есть на сайте. Здесь можно увидеть, какие именно платформы поддерживают плагины: мобильные, web или desktop.
Во Flutter Web существует четыре варианта, где можно хранить данные: Cookies, Local Storage, Session Storage и IndexedDB. Давайте сравним их и выделим сильные и слабые стороны каждого.
Сравнение вариантов для хранения данных
Cookies | Local Storage | Session Storage | IndexedDB | |
Тип данных | Пары ключ-значение, строковые | Пары ключ-значение, строковые | Пары ключ-значение, строковые | Пары ключ-значение, разные типы данных |
Отправка вместе с запросом | Да | Нет | Нет | Нет |
Размер | 4 Кб | 5 Мб | 5 Мб | Зависит от браузера и свободного места на диске |
Время хранения | Expiry time / не ограничено | Не ограничено | До закрытия вкладки | Не ограничено |
Назначение | Персонализации и отслеживания действий пользователя | Локальное хранилище небольших объемов данных между сессиями | Локальное хранилище небольших объемов данных в пределах одной вкладки | Локальное хранилище больших объемов данных между сессиями |
Давайте разберем подробнее:
window
. Данные здесь, как и в Сookies, хранятся в виде строковых пар ключ-значение, а время их хранения не ограничено. Для каждого домена браузер создает свой Local Storage. Доступ к данным в происходит гораздо быстрее, чем в Cookies. Сохранение данных на Flutter Web может быть реализовано такими способами:
dart:html
, dart:js
, dart:indexed_db
.Разберем этот кейс на примере 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);
В этом случае можно использовать 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. Они используются для обозначения файлов и объектов, необходимых для корректной работы веб-приложения.Во время использования веб-приложения пользователем, ресурсы будут кэшироваться в его браузере и при обновлении приложения могут отображаться некорректно. Это происходит, потому что в браузер загружаются варианты ресурсов из предыдущей версии приложения (до его обновления).
Ситуацию можно исправить несколькими способами:
На практике оба подхода применяются одинаково успешно.
После сборки веб-приложения вы можете столкнуться с проблемой некорректного отображения интерфейса, который изначально разрабатывался для мобильных версий (неправильное масштабирование, разрывание текста). Чтобы избежать этого, приложение нужно сделать респонсивным — применить к нему дизайн, характерный для внешнего вида веба. При этом оставить мобильный вариант для мобильной версии.
Когда пользователь поворачивает девайс горизонтально или изменяет размер страницы в браузере, приложение должно корректно масштабироваться. Как отследить это?
Нужно самостоятельно проверить какое-либо условие (например, размер экрана), а затем — используя 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.
Чтобы постоянно не прописывать 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 для веб-приложений:
Навигация во Flutter Web может быть реализована тремя способами:
RouteInformationParser
, который можно унаследовать и написать свой URL Parser для перехода на нужный скрин.Описанные методы навигации на Flutter далеки от идеала, но они активно обновляются, так что советую следить за новинками и читать патчноуты.
Среди распространенных «болячек», от которых до сих пор не излечился Flutter Web, часто выделяют следующие:
Многие из этих фич планируется реализовать в ближайшее время, и, надеемся, к моменту выхода статьи этот список поредеет 🙂
Переход с Flutter на Flutter Web может показаться непривычным, но рассматривайте это как профессиональный челлендж. Тем более интерес, который IT-комьюнити проявляет к его развитию, подтверждает востребованность этой технологии в веб-разработке.
В благословенные офисные времена, когда не было большой войны и коронавируса, люди гораздо больше общались…
Вот две истории из собственного опыта, с тех пор, когда только начинал делать свою карьеру…
«Ты же программист». За свою жизнь я много раз слышал эту фразу. От всех. Кто…
Отличные новости! Если вы пропустили, GitHub Copilot — это уже не отдельный продукт, а набор…
Несколько месяцев назад мы с командой Promodo (агентство инвестировало в продукт более $100 000) запустили…
Пару дней назад прочитал сообщение о том, что хорошие курсы могут стать альтернативой классическому образованию.…