Flutter Web. Часть 1
Привет, меня зовут Максим, я Flutter-разработчик в компании Surf.
Flutter позволяет собирать одну кодовую базу не только в мобильные и десктопные приложения, но и в веб-приложения. Но как работает Flutter Web и есть ли особенности взаимодействия с платформой? Разбираемся с этим в серии статей. И это первая.
Зачем нужен Flutter Web
Flutter Web — не замена Html/Css/Js. Он совершенно не подходит для создания классических веб-сайтов. Блоги, портфолио, доски объявлений, лендинги — там Flutter себя не проявит.
Конечно, всё это можно сделать, но цена будет слишком высока. Это и внушительный вес готового приложения, и не нативное поведение для веб-среды, и отсутствие SEO, и высокие требования к производительности клиента.
При этом Flutter — прекрасный инструмент для разработки веб-приложений.
Например, если вы — банк, и вас выгнали из магазина приложений.
Или мессенджер, и хотите быть доступны клиентам не только на мобильных устройства, но и встраиваться в веб-страницы CRM?
Или же вы — облачный векторный редактор и хотите дать возможность работать пользователям без установки, в любой ос и из любой точки мира прямо в браузере?
Мы не будем рассматривать особенности разработки приложений на Flutter — они не отличаются от разработки для мобильных ОС. Мы сконцентрируемся на взаимодействии с платформой. В нашем случае, это — веб-браузер и нативный для этой среды язык — JavaScript.
Browser API
Dart планировался как «убийца» js, но что-то пошло не так.
Он не стал популярен в сообществе. А специальная версия Google Chrome с нативной поддержкой Dart в качестве основного языка в итоге переродилась во Flutter.
Это сомнительное прошлое до сих пор служит на пользу Flutter и позволяет напрямую вызывать Browser API. Для этих целей есть пакет web, который в Dart 3.4 пришёл на смену целому пулу встроенных в dart-sdk плагинов:
Все эти пакеты помечены как устаревшие, их поддержка скоро будет ограничена. И в конечном итоге они будут удалены из Dart SDK.
Browser API — мощный инструмент, который позволяет нам взаимодействовать с браузером. В каком-то смысле при разработке мобильных приложений мы можем воспринимать его как Android/IOS SDK.
Он позволяет получить доступ к хранилищу кеша, Indexed DB, адресной строке, микрофону, веб-камере. Подробнее о возможностях Browser API тут
Вызов Browser API из Dart
Начнём знакомство с Flutter Web. Для этого добавим одноимённый пакет в наш проект.
dart pub add web
Импортируем зависимость в файл.
import 'package:web/web.dart' as html;
Префикс HTML используется для избегания конфликтов имен с другими пакетами и встроенными возможностями языка. Самое простое, что мы можем сделать, это узнать User-Agent пользователя и определить веб-браузер, с которого он использует наше приложение.
Text('Browser: ${userAgent2Browser(html.window.navigator.userAgent)}')
Так мы получаем объект Window из пакета web, используя префикс html.
Из Navigator мы получаем User-Agent и достаём из него название браузера с помощью метода userAgent2Browser, который реализовали сами.
Просто, не правда ли?
URL launcher
Теперь посложнее. Откроем внешнюю ссылку в соседней вкладке. Для реализации аналога пакета url_launcher в браузере достаточно обратится к объекту Window и передать нужный веб-адрес
html.window.open('https://www.google.com', 'Google')
Отметим, что метод open () позволяет передавать в него параметры и открывать новую вкладку не просто вкладкой, а отдельным окном, и даже задать размеры или положение.
html.window.open('https://www.google.com', 'Google', 'left=100, top=100, width=500, height=300, 'popup');
Alert
Не менее классический пример простого использования Browser API — отображение диалога.
final isAccess = html.window.confirm("Open Google?");
if (isAccess) {
html.window.open("https://www.google.com", "Google");
} else {
html.window.alert("=(");
}
Эти примеры показывают, что взаимодействовать с браузером из Dart достаточно просто.
Но такой подход покрывает только базовые возможности. Если мы хотим сделать нечто более интересное и сложное, нам не обойтись без пакета dart: js_interop
JavaScript interoperability
JavaScript interoperability — это механизм, который обеспечивает совместимость Dart и JavaScript. Он позволяет двум языкам обмениваться данными, вызывать методы друг друга напрямую и с помощью дополнительных обёрток и интерфейсов.
Обеспечивается этот механизм пакетом dart: js_interop, который уже включен в Dart SDK. Для использования достаточно добавить его импорт.
import 'dart:js_interop';
Этот пакет содержит множество объектов и методов, помогающих выстроить взаимодействие между js и dart.
Пакеты dart: js_interop и dart: js_interop_unsafe в Dart 3.4 пришли на смену package: js, dart: js и dart: js_util.
Именно их сегодня используют разработчики., Они позволяют не только взаимодействовать с js-слоем, но и подготовить веб-приложение к компиляции в WebAssembly.
У старых же пакетов ограниченная поддержка. И есть вероятность, что их удалят в новых версиях языка. Так что если вы используете эти пакеты, самое время задуматься о миграции. Кому нужны проблемы в будущем?
Вызов Js из Dart
Начнём со знакомого метода window.alert()
// Указываем название JS-метода
// @JS('window.alert')
// на самом деле вызов можно упростить и вызывать alert
// на прямую, браузер автоматически ищет методы и объекты в window
@JS('alert')
// Указываем что метод внешний
// и объявляем входные параметры и возвращаемый тип
external void showAlert(String message);
...
ElevatedButton(
onPressed: () {
// используем так, будто это метод dart
showAlert("Hello from dart");
},
child: const Text("alert"),
),
...
Возможности dart: js_interop позволяют нам описать интерфейс этого метода. И вызывать его так, будто это dart-метод.
И здесь нам даже не нужно преобразовывать типы, все сделают за нас в процессе компиляции.
А что, если нужно наоборот? Попробуем вызвать метод Dart из Js.
Вызов Dart из JS
/// Объявляем метод
void dartPrint(String message) {
print('JS say: $message');
}
/// Регистрируем в текущем контексте
html.window.setProperty(
'printOnDart'.toJS, // Даем название
dartPrint.toJS, // Указываем, какой метод будет вызван
);
Здесь обратите свое внимание на геттер .toJS его предоставляет библиотека dart: js_interop, он служит для приведения типов из Dart в Js.
Похожим образом мы можем регистрировать коллбэк и обрабатывать события js на стороне Dart.
void onWindowEvent(html.Event e) {
print(e.type);
}
html.window.onblur = onWindowEvent.toJS;
html.window.onfocus = onWindowEvent.toJS;
Вот так, например, мы можем отслеживать активность вкладки браузера, в которой запущено наше приложение.
Использование JS библиотек
Усложним и разнообразим наши примеры — используем стороннюю js-библиотеку для отображения push-уведомлений из dart.
Для начала подключим её в web/index.html
— добавим её в в блок
<...>
Теперь убедимся, что библиотека подключена правильно. Для этого воспользуемся консолью DevTools и вызовем метод библиотеки
Вызов библиотек не отличается от того, что мы уже пробовали с объектом window. Но тут есть одно усложнение. Взглянем на описание метода Push.create()
в документации:
Push.create("Hello world!", {
body: "How's it hangin'?",
icon: "/icon.png",
timeout: 4000,
onClick: function () {
window.focus();
this.close();
}
});
Метод принимает в себя 2 параметра:
1. Строка, которая будет заголовком оповещения.
2. Некоторая структура, в которой находятся именованные параметры body
, icon
, timeout
и onClick
.
Может показаться , что второй параметр очень похож на
Map
. Но это не совсем так.При использовании JS-interop мы налаживаем коммуникацию между двумя разными языками, с разной системой типов. Разработчики Dart SDK предусмотрели авто-приведение для простых и некоторых промежуточных типов данных:
базовые типы типы dart:
void
,bool
,num
,double
,int
,String
;ссылки на скомпилированные в js объекты dart
ExternalDartReference
;JSAny
и наследники, стандартные типы js (JSString
,JSFunction
,JSArray
и другие).
Полный список тут
Типа Map в этом списке нет. Но мы можем преобразовать Map
в JSON
(JavaScript Object Notation), что и станет эквивалентом Map
в js.
Опишем интерфейс для взаимодействия с js-объектом Push
:
/// Указываем что данный объект является JS объектом
@JS()
/// Для использования, нам не нужен инстанс этого объекта,
/// мы вызываем только статические методы
@staticInterop
class Push {
/// Объявляем интерфейс внешнего метода
/// В данном случае нам нужно помочь dart
/// определится с типами.
/// JSAny? - это родительский тип для всех типов в JS
/// Можем считать его аналогом Object? в Dart
external static void create(String title, JSAny? options);
}
Нам остаётся только преобразовать Map
в JSON
:
Push.create(
'Title', {
'body': 'Hello, World!',
}.jsify()
);
Этот подход работает, но использовать его нужно осторожно. Метод jsify()
и dartify()
, служащие для обратного преобразования, скорее всего, будут удалены в ближайших версиях библиотеки dart:js_interop
.
Геттер toJS
недоступен для структур, да и указывать ключи строкой — не самая безопасная идея — легко опечататься. Так как преобразовывать такие объекты?
Описываем структуру и отмечаем ее как внешнюю @JSExport()
:
@JSExport()
class Options {
final String? body;
final String? icon;
Options({
this.body,
this.icon,
});
}
Добавляем расширение типа для JSObject
с теми же полями и типами данных, что у основной структуры:
extension type OptionsExternal(JSObject _) implements JSObject {
external String? body;
external String? icon;
}
Используем метод для регистрации объекта dart как js-объекта createJSInteropWrapper
:
Push.create(
'Title',
createJSInteropWrapper(
Options(
body: 'Hello, World!',
icon: 'path.to/icon.png',
),
) as OptionsExternal,
);
Конец первой части
Межъязыковое взаимодействие Js-Dart устроено достаточно просто. Но писать большой функционал только на Dart не всегда удобно. Как минимум — из-за отсутствия подсказок IDE о доступных методах в Js-объектах. Как максимум — из-за многословного преобразования комплексных объектов. Большие модули гораздо удобнее разрабатывать на нативном языке (Js) и оставлять простой интерфейс для взаимодействия с Dart.
В следующей части статьи разберёмся с тем, как разрабатывать собственные библиотеки используя TypeScript, и подготовим приложение к кроссплатформенной компиляции.
Больше полезного про Flutter — в Telegram-канале Surf Flutter Team.
Кейсы, лучшие практики, новости и вакансии в команду Flutter Surf в одном месте. Присоединяйтесь!