Flutter Webからウェブコンポーネントを使う
この記事はjig.jp Advent Calender 2024、1日目の記事です。
今年も始まりました、jig.jp AdventCalendar!
毎年参加しています、ダイスです。1日目はFlutterの技術紹介になります。
Flutterはマルチプラットフォーム対応しており、Webアプリにも対応しています。
そんなFlutter Webからウェブコンポーネントを使う方法を、アプリを作る流れで紹介します。
この方法は、Flutter Webではまだサポートされていない技術や、Webの資産を再利用したいときに便利です。
開発環境
新しいFlutterアプリケーションプロジェクトを準備しておきます。
このプロジェクトでは、以下のようなアプリを作成します。
このアプリは、ボタンをクリックしたらイベントを送り合う簡単なアプリです。
開発環境は以下になります。
- Flutter 3.24.3
- Dart 3.5.3
- web 1.1.0
ウェブコンポーネントの準備
まずはFlutter Webに配置するウェブコンポーネントを用意します。
新しいJavaScriptファイルをweb/simple-button.js
に作成します。
作成したファイルに、ウェブコンポーネントを定義するコードを追加しましょう。
customElements.define(
"simple-button",
class extends HTMLElement {
static observedAttributes = ["text"];
constructor() {
super();
// ボタンタグの作成
const buttonElem = document.createElement("button");
buttonElem.type = "button";
buttonElem.textContent = this.getAttribute("text");
// シャドウDOMに登録
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(buttonElem);
}
attributeChangedCallback(name, _, newValue) {
// text属性の変更をボタンに反映
if (name === "text") {
this.shadowRoot.querySelector("button").textContent = newValue;
}
}
}
);
このコンポーネントは、button
タグでtext
属性に指定した文字を表示します。
また、attributeChangedCallback
で属性の変更検知をしています。この変更検知があることで、Flutter Webからの変更が反映できます。
ちなみに、ウェブコンポーネントの実装はこちらのリポジトリを参考にしました。
では早速このコンポーネントを配置してみましょう。
Flutter Webにウェブコンポーネントを配置
まずは、webパッケージを追加します。
以下のコマンドでパッケージを追加しましょう。
dart pub add web
次にweb/index.html
でsimple-button.js
ファイルを読み込みます。
<body>
<script type="module" src="./simple-button.js"></script>
<script src="flutter_bootstrap.js" async></script>
</body>
Flutterでは、HtmlElementView
を使ってコンポーネントを配置します。
lib/component.dart
を作成し、以下のコードを追加しましょう。
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
class MyWebComponent extends StatelessWidget {
const MyWebComponent({super.key});
Widget build(BuildContext context) {
return HtmlElementView.fromTagName(
tagName: 'simple-button',
onElementCreated: (object) {
final element = object as web.Element;
element.setAttribute('text', 'Flutterに送る');
},
);
}
}
simple-button
タグをHtmlElementView
でウィジェット階層に埋め込んでいます。
このウィジェットについては、次に詳しく説明します。
まずはmain.dart
から、コンポーネントを呼び出して表示してみましょう。
import 'package:flutter/material.dart';
import './component.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('ウェブコンポーネント'),
),
body: const MyWebComponent(),
),
);
}
}
これでウェブコンポーネントを配置できました!
HtmlElementViewでウェブコンポーネントの埋め込み
Flutter Webは、独自のレンダリングエンジンを用いて、ウィジェットをCanvasにレンダリングしています。
開発者モードでDOM要素を表示すると、canvas
タグにウィジェットがレンダリングされていることが確認できます。
そのため、Flutter Webでウェブコンポーネントを含むHTMLを自由に扱うには、ウィジェット階層にHTMLを埋め込む必要があります。
HtmlElementView
は、まさにそのためのウィジェットです。
このウィジェットを使って埋め込まれたHTMLは、他のFlutter Widgetと同じようにレイアウトされます。
つまり、ウェブコンポーネントをウィジェット階層に埋め込むことでレイアウトを実現しています。
次は、Flutter Webとウェブコンポーネント間で通信できるようにしてみましょう。
Flutter Webとウェブコンポーネントの通信
CustomEvent
を使って相互に通信します。
Flutter Webからウェブコンポーネントへの送信方法と、その逆を実装していきましょう。
Flutter Webからウェブコンポーネントへ通信
Flutter Webからは、dispatchEvent
でイベントを送信します。
lib/send_web_button.dart
ファイルを作成し、コードを追加しましょう。
import 'dart:js_interop';
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
class SendWebButton extends StatelessWidget {
const SendWebButton({super.key});
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
// ウェブコンポーネントにデータを送信
web.document.dispatchEvent(
web.CustomEvent(
'from-flutter-to-web',
web.CustomEventInit(
detail: {'data': 'hello, web!'}.jsify(),
),
),
);
},
child: const Text('Webに送る'),
);
}
}
ウェブコンポーネントは、addEventListener
でイベントを受信します。
ここでは、simple-button.js
のコンストラクタにコードを追加します。
// ... 省略
constructor() {
super();
document.addEventListener("from-flutter-to-web",
(e) => console.log(e.detail.data) // hello, web!
);
// ... 省略
これでFlutterからコンポーネントへの通信ができました。
引き続き、コンポーネントからFlutterへの通信を実装していきましょう。
ウェブコンポーネントからFlutter Webへ通信
使うメソッドは先程と同様です。
ウェブコンポーネントからは、dispatchEvent
でイベントを送信します。
simple-button.js
に、ボタンのonclick
でイベントを送信するコードを追加しましょう。
buttonElem.onclick = () => {
this.dispatchEvent(
new CustomEvent('from-web-to-flutter', {
bubbles: true,
composed: true,
detail: 'hello, flutter!',
})
);
}
コンポーネントの外からもイベントを購読できるように、bubbles
とcomposed
を有効にしています。
Flutter Webは、addEventListener
でイベントを受信します。
ここでは、main.dart
に受信するコードを追加しましょう。
簡単なアプリにするため、購読を解除するなどの制御は省略しています。
void main() {
void startListener(web.CustomEvent event) {
print(event.detail); // hello, flutter!
}
web.window.addEventListener('from-web-to-flutter', startListener.toJS);
runApp(const MyApp());
}
最後に、main.dart
にコンポーネントを配置するコードを追加します。
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ウェブコンポーネント')),
body: const Column(
children: [
SendWebButton(), // Flutterのボタン
Flexible(child: MyWebComponent()), // コンポーネントのボタン
],
),
),
);
}
}
Flutterのボタンとコンポーネントのボタンを押してみましょう。
それぞれ送信した文字列がログに出力されていれば成功です!
これで相互に通信できました!
まとめ
Flutter Webからウェブコンポーネントを使う方法を紹介しました。
方法として、HtmlElementView
で配置し、CustomEvent
でデータを送受信しました。
今回は簡単なHTMLタグだけでしたが、npmのパッケージを活用して、Flutter Web単体では難しい機能を実装することもできます。ぜひお試しください!
Discussion