Привет, меня зовут Максим, я 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
— добавим её в в блок <head>
<...> <script src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.8/push.min.js"></script> </head>
Теперь убедимся, что библиотека подключена правильно. Для этого воспользуемся консолью 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<String, dynamic>
. Но это не совсем так. -
При использовании 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<T>(objectInstance)
:
Push.create( 'Title', createJSInteropWrapper<Options>( 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 в одном месте. Присоединяйтесь!
ссылка на оригинал статьи https://habr.com/ru/articles/840606/
Добавить комментарий