🤝

Flutter Webからウェブコンポーネントを使う

2024/12/01に公開

この記事はjig.jp Advent Calender 2024、1日目の記事です。
今年も始まりました、jig.jp AdventCalendar!
毎年参加しています、ダイスです。1日目はFlutterの技術紹介になります。


Flutterはマルチプラットフォーム対応しており、Webアプリにも対応しています。

https://flutter.dev/multi-platform/web

そんなFlutter Webからウェブコンポーネントを使う方法を、アプリを作る流れで紹介します。
この方法は、Flutter Webではまだサポートされていない技術や、Webの資産を再利用したいときに便利です。

開発環境

新しいFlutterアプリケーションプロジェクトを準備しておきます。
このプロジェクトでは、以下のようなアプリを作成します。

flutter-and-component

このアプリは、ボタンをクリックしたらイベントを送り合う簡単なアプリです。
開発環境は以下になります。

  • Flutter 3.24.3
  • Dart 3.5.3
  • web 1.1.0

ウェブコンポーネントの準備

まずはFlutter Webに配置するウェブコンポーネントを用意します。
新しいJavaScriptファイルをweb/simple-button.jsに作成します。

作成したファイルに、ウェブコンポーネントを定義するコードを追加しましょう。

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からの変更が反映できます。

ちなみに、ウェブコンポーネントの実装はこちらのリポジトリを参考にしました。

https://github.com/mdn/web-components-examples

では早速このコンポーネントを配置してみましょう。

Flutter Webにウェブコンポーネントを配置

まずは、webパッケージを追加します。
以下のコマンドでパッケージを追加しましょう。

dart pub add web

次にweb/index.htmlsimple-button.jsファイルを読み込みます。

index.html
<body>
  <script type="module" src="./simple-button.js"></script>
  <script src="flutter_bootstrap.js" async></script>
</body>

Flutterでは、HtmlElementViewを使ってコンポーネントを配置します。
lib/component.dartを作成し、以下のコードを追加しましょう。

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から、コンポーネントを呼び出して表示してみましょう。

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(),
      ),
    );
  }
}

component-only

これでウェブコンポーネントを配置できました!

HtmlElementViewでウェブコンポーネントの埋め込み

Flutter Webは、独自のレンダリングエンジンを用いて、ウィジェットをCanvasにレンダリングしています。
開発者モードでDOM要素を表示すると、canvasタグにウィジェットがレンダリングされていることが確認できます。

flutter-web-canvas

そのため、Flutter Webでウェブコンポーネントを含むHTMLを自由に扱うには、ウィジェット階層にHTMLを埋め込む必要があります。
HtmlElementViewは、まさにそのためのウィジェットです。

https://api.flutter.dev/flutter/widgets/HtmlElementView-class.html

このウィジェットを使って埋め込まれたHTMLは、他のFlutter Widgetと同じようにレイアウトされます。

つまり、ウェブコンポーネントをウィジェット階層に埋め込むことでレイアウトを実現しています。
次は、Flutter Webとウェブコンポーネント間で通信できるようにしてみましょう。

Flutter Webとウェブコンポーネントの通信

CustomEventを使って相互に通信します。
Flutter Webからウェブコンポーネントへの送信方法と、その逆を実装していきましょう。

Flutter Webからウェブコンポーネントへ通信

Flutter Webからは、dispatchEventでイベントを送信します。
lib/send_web_button.dartファイルを作成し、コードを追加しましょう。

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のコンストラクタにコードを追加します。

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でイベントを送信するコードを追加しましょう。

simple-button.js
buttonElem.onclick = () => {
  this.dispatchEvent(
    new CustomEvent('from-web-to-flutter', {
      bubbles: true,
      composed: true,
      detail: 'hello, flutter!',
    })
  );
}

コンポーネントの外からもイベントを購読できるように、bubblescomposedを有効にしています。

Flutter Webは、addEventListenerでイベントを受信します。
ここでは、main.dartに受信するコードを追加しましょう。
簡単なアプリにするため、購読を解除するなどの制御は省略しています。

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にコンポーネントを配置するコードを追加します。

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-and-component

これで相互に通信できました!

まとめ

Flutter Webからウェブコンポーネントを使う方法を紹介しました。
方法として、HtmlElementViewで配置し、CustomEventでデータを送受信しました。

今回は簡単なHTMLタグだけでしたが、npmのパッケージを活用して、Flutter Web単体では難しい機能を実装することもできます。ぜひお試しください!

jig.jp Engineers' Blog

Discussion