Flutter エレメントエンべディング・アプリを ウェブサイト内に入れられるの力！AngularやReactまでもできる！

　「Flutterforward 2023」のイベント祭に初めて「エレメントエンべディング」を発表された、そういう機能ができて、驚きました。

　早速色んな使い方を思い浮かびました、例えばホームページ上でユーザーがアプリを直接試すことができます。もう一つのアイデアとしてドキュメンテーションのページで実際の例が見られます。

---

 　Flutter エレメントエンべディングのメリットは:
 - 実際のアプリを直接使用することができるんです
 - アプリとウェブサイト間で情報を共有することが出来ます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3522789/b8a345de-3e50-6535-f16c-43724ab15db5.png)
この例は[こちら](https://flutter-show-docs.vercel.app/)で見られます

## 実装方法
　実は実装方法は簡単です。まず、普通のHTMLウェブサイトの例で説明します。全部のコードを見たいなら、こちらでチェックしてください

https://github.com/lucas-goldner/Element-Embedding-Presentation/tree/Flutterconnection-2024/example_website/red_counter_homepage

でも、最初から始めましょう

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Embedded Flutter Website</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <header>
      <h1>See the Flutter app below!</h1>
    </header>
    <div>
      <main>
        <section>
          <div id="flutter_host" style="height: 812px; width: 375px">
            Loading...
          </div>
        </section>
      </main>
    </div>
  </body>
</html>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3522789/635d81a2-e1a4-b944-5108-26aca0865af4.png)

1. アプリをウェブバージョンビルドをしないといけません

```bash
$ flutter build web
```

2. 次のステップはウェブサイトにスクリプトをロードします。まず、flutterのエンジンスクリプトとロードを追加します。flutter.jsはflutterのエンジンの開始コードが入っています

```html
<script src="./あなたのflutterアプリ/flutter.js" defer></script>
```

　エンジンの「ロード」イベント時に別のスクリプトで自分のアプリをロード出来ます。これを使って、ウェブサイト内にアプリを入れます。エンジンは色んな設定がありますので、[こちら](https://docs.flutter.dev/platform-integration/web/initialization)で確認してください

```html
<script>
      window.addEventListener("load", function (ev) {
        const basePath = "./あなたのflutterアプリ/";

        _flutter.loader.loadEntrypoint({
          entrypointUrl: basePath + "main.dart.js",
          onEntrypointLoaded: async function (engineInitializer) {
            let appRunner = await engineInitializer.initializeEngine({
              assetBase: basePath,
              hostElement: document.querySelector("#flutter_host"),
            });
            await appRunner.runApp();
          },
        });
      });
    </script>
```

　ウェブサイトコードに追加したら、この結果をもらえます

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Embedded Flutter Website</title>
    <link rel="stylesheet" href="./styles.css" />
    <script src="./flutter_app/flutter.js" defer></script>
    <script>
      window.addEventListener("load", function (ev) {
        const basePath = "./flutter_app/";

        _flutter.loader.loadEntrypoint({
          entrypointUrl: basePath + "main.dart.js",
          onEntrypointLoaded: async function (engineInitializer) {
            let appRunner = await engineInitializer.initializeEngine({
              assetBase: basePath,
              hostElement: document.querySelector("#flutter_host"),
            });
            await appRunner.runApp();
          },
        });
      });
    </script>
  </head>
  <body>
    <header>
      <h1>See the Flutter app below!</h1>
    </header>
    <div>
      <main>
        <section>
          <div id="flutter_host" style="height: 812px; width: 375px">
            Loading...
          </div>
        </section>
      </main>
    </div>
  </body>
</html>
```

　これで全部です!その方法とおりに、実装したら、これが見られます:

![image.png](https://miro.medium.com/v2/resize:fit:1400/format:webp/1*hXERG11KKJGWC_hR4iphOA.gif)

## Reactを使ったらどうする

　前の述べたらようにReactウェブサイトで実装したら、問題を見つけました。この[ガイド](https://github.com/flutter/flutter/issues/123940)のおかげで、結局にエンべディングができました。

1: まずこのコマンドでビルドを書かないといけません

```bash
$ flutter build web --profile --dart-define=Dart2jsOptimization=O0
```
　縮小を点けませんでしたので、このアウトプットは普通の比べて、サイズより大きい。その理由で依頼人は大きいバンドルをダウンロドをしないと

2: webファイルは`/build/web/`からreactの`public` ファイルにコピーしてください。このプロジェクトでは`web`ファイルは`flutter`に変えましたので、全部は`あなたのホスト/flutter/`からアクセスことができます.次に`public/flutter/main.dart.js`を開けて、`t1`は自分のpathで変えないといけません

```js
// これを探して
    getAssetUrl$1(asset) {
      var t1, fallbackBaseUrl, t2;
      if (A.Uri_parse(asset).get$hasScheme())
        return A._Uri__uriEncode(B.List_5Q7, asset, B.C_Utf8Codec, false);
      t1 = this._assetBase; <----
    }

// こちらに変更しないと
    getAssetUrl$1(asset) {
      var t1;
      if (A.Uri_parse(asset, 0, null).get$hasScheme())
        return A._Uri__uriEncode(B.List_5Q7, asset, B.C_Utf8Codec, false);
      t1 = "/flutter/"; <----
    }
```

3: 同じ方法で`flutter.js`に変更して

```js
// これを探して
function getBaseURI() {
  const base = document.querySelector("base");
  return (base && base.getAttribute("href")) || "";
}

// 同じpathでこちらに変更しないと
function getBaseURI() {
  return "/flutter/";
}
```
4: publicファイルで新しい`flutter_init.js`を作って、このコードをペストをして

```js
window._stateSet = function () {};
window.addEventListener("load", function (ev) {
  let target = document.querySelector("#flutter_target");
  _flutter.loader.loadEntrypoint({
    onEntrypointLoaded: async function (engineInitializer) {
      let appRunner = await engineInitializer.initializeEngine({
        hostElement: target,
      });
      await appRunner.runApp();
    },
  });
});
```

5: 今reactのプロジェクトでreact-helmet-asyncを追加して

```bash
$ yarn add react-helmet-async
```

6: 準備が終わりました！ReactのアプリはHelmetProviderで包んで

```js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { HelmetProvider } from "react-helmet-async"
import './index.css'

const helmetContext = {};

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
    <React.StrictMode>
        <HelmetProvider context={helmetContext}>
            <App />
        </HelmetProvider>
    </React.StrictMode>
)
```

7: 最後にHelmetのコンポーネントを追加して、`flutter_init.js`と`flutter.js`ロドしたら、flutterのアプリが見られます

```js
import { Helmet } from "react-helmet-async";

function App() {
  return (
    <>
      <Helmet>
        <script src="/flutter/flutter.js" defer></script>
        <script src="/flutter_init.js" defer></script>
      </Helmet>
      <div
        style={{ aspectRatio: 9 / 19.5 }}
        id="flutter_target"
        className="h-full"
      ></div>
    </>
  );
}
```

---

　この手続きは結構大変だし、新しい変化するたびに、もう一度あのステップたちをしないとから、このスックリプトを書きました。CIに使ってください！

```bash
#!/bin/bash

# Paths relative to the script's location
SOURCE_DIR="../fluttershow_app/"
BUILD_DIR="../fluttershow_app/build/web/"
DESTINATION_DIR="./public/flutter"
MAIN_DART_FILE="$DESTINATION_DIR/main.dart.js"
FLUTTER_FILE="$DESTINATION_DIR/flutter.js"

# Navigate to the SOURCE_DIR and run steps to build the flutter web app
cd "$SOURCE_DIR"
flutter build web --profile --dart-define=Dart2jsOptimization=O0

# Check for any errors during the flutter build
if [ $? -ne 0 ]; then
    echo "Error: Failed to build flutter web app in $SOURCE_DIR"
    exit 1
fi

# Change directory back to the script's location
cd - 

# Check if the build directory exists
if [ ! -d "$BUILD_DIR" ]; then
    echo "Error: Source directory $BUILD_DIR does not exist."
    exit 1
fi

# Remove the old flutter directory if it exists
if [ -d "$DESTINATION_DIR" ]; then
    echo "Removing old flutter directory..."
    rm -rf "$DESTINATION_DIR"
fi

# Copy the source to the destination
echo "Copying $BUILD_DIR to $DESTINATION_DIR..."
cp -r "$BUILD_DIR" "$DESTINATION_DIR"

# Check for any errors during the copy
if [ $? -ne 0 ]; then
    echo "Error: Failed to copy $BUILD_DIR to $DESTINATION_DIR"
    exit 1
fi

# Modify the flutter/main.dart.js file
if [ -f "$MAIN_DART_FILE" ]; then
    echo "Modifying $JS_FILE..."
    sed -i '' 's|t1 = this._assetBase;|t1 = "/flutter/";|g' "$MAIN_DART_FILE"
else
    echo "Warning: $MAIN_DART_FILE does not exist. Skipping modification."
fi

if [ -f "$FLUTTER_FILE" ]; then
    echo "Modifying $FLUTTER_FILE..."

    # Remove the specific line
    sed -i '' '/const base = document.querySelector("base");/d' "$FLUTTER_FILE"

    # # Replace the specific line
    sed -i '' 's|return (base && base.getAttribute("href")) \|\| "";|return "/flutter/";|g' "$FLUTTER_FILE"

else
    echo "Warning: $FLUTTER_FILE does not exist. Skipping modification."
fi

echo "Updated ✨"
```

## ウェブサイトからアプリの状態を変更してみましょう

　この例では、ウェブサイトにボタンとテキストフィルドを使いしました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3522789/1577fca4-24f0-404e-503e-0637996e196f.png)

ウェブサイトからアプリの状態を変更するために、「JS Interop」を使います。まず、Dartコードをウェブサイトからアクセスできるように設定します。

1:「js」パッケージを追加します
```
$ flutter build web
```

1:「js」パッケージを追加します
```
$ flutter build web
```

2: dartとjsのコミュニケーション

　アプリと仕様によって、このステップは異なっています。ですから、この[リポジトリ](https://github.com/lucas-goldner/Element-Embedding-Presentation/blob/Flutterconnection-2024/example_website/red_counter_app/lib/main.dart)では例が見られます。Decoratorパターンを使ってアプリの状態と関数をエクスポートします。

```dart
@js.JSExport()
class _MyHomePageState extends State<MyHomePage> {
  final _streamController = StreamController<void>.broadcast();
  int _counterScreenCount = 0;

  @js.JSExport()
  void addHandler(void Function() handler) {
    _streamController.stream.listen((event) {
      handler();
    });
  }

  @js.JSExport()
  int get count => _counterScreenCount;

  ...
}
```

　「initState」関数を見てみましょう。ここで、2つのオブジェクトをエクスポートします。まず、exportを使って現在の状態をエクスポートし、「_appState」にアサインをします。次に、「_stateSet」関数をウェブサイトのコードで使用できるようにします。

```dart
 @override
  void initState() {
    super.initState();
    final export = js_util.createDartExport(this);
    js_util.setProperty(js_util.globalThis, '_appState', export);
    js_util.callMethod<void>(js_util.globalThis, '_stateSet', []);
  }
```

　普通にカウンターのアプリにstreamのコントロラは必要がありませんけど、この場合は、カウンタを変わるたびに、ウェブサイトのテキストフィルドを変更したいです

3. dartのコードはjsにアサインをします

　ウェブサイトのコードを見てみましょう。まず、「_stateSet」関数を設定します。この例では、プリント機能のために使っています。次に、ブラウザのグローバル「window」オブジェクトに「_appState」を追加します。残りのコードでは、入力フィールドとDartの関数を組み合わせます

```js
// Sets up a channel to JS-interop with Flutter
(function () {
  "use strict";
  // This function will be called from Flutter when it prepares the JS-interop.
  window._stateSet = function () {
    window._stateSet = function () {
      console.log("Calls _stateSet once!");
    };

    // The state of the flutter app, see `class _MyAppState` in lib/main.dart.
    let appState = window._appState;

    let valueField = document.querySelector("#value");
    let updateState = function () {
      valueField.value = appState.count;
    };

    // Register a callback to update the HTML field from Flutter.
    appState.addHandler(updateState);

    // Render the first value (0).
    updateState();

    let incrementButton = document.querySelector("#increase-btn");
    incrementButton.addEventListener("click", (event) => {
      appState.increment();
    });
  };
})();
```

　このコンテンツで新しいの`main.js作りました

```js
// Sets up a channel to JS-interop with Flutter
(function () {
  "use strict";
  // This function will be called from Flutter when it prepares the JS-interop.
  window._stateSet = function () {
    window._stateSet = function () {
      console.log("Calls _stateSet once!");
    };

    // The state of the flutter app, see `class _MyAppState` in lib/main.dart.
    let appState = window._appState;

    let valueField = document.querySelector("#value");
    let updateState = function () {
      valueField.value = appState.count;
    };

    // Register a callback to update the HTML field from Flutter.
    appState.addHandler(updateState);

    // Render the first value (0).
    updateState();

    let incrementButton = document.querySelector("#increase-btn");
    incrementButton.addEventListener("click", (event) => {
      appState.increment();
    });
  };
})();
```

　そのスクリプトをロドします

```js
<script src="./main.js" defer></script>
```

　最後の結果はこれになります

```js
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Interactive Red Counter - Revolutionary Counting App</title>
    <link rel="stylesheet" href="./styles.css" />
    <script src="./flutter_app/flutter.js" defer></script>
    <script src="./main.js" defer></script>

    <script>
      window.addEventListener("load", function (ev) {
        const basePath = "./flutter_app/";

        _flutter.loader.loadEntrypoint({
          entrypointUrl: basePath + "main.dart.js",
          onEntrypointLoaded: async function (engineInitializer) {
            let appRunner = await engineInitializer.initializeEngine({
              assetBase: basePath,
              hostElement: document.querySelector("#flutter_host"),
            });
            await appRunner.runApp();
          },
        });
      });
    </script>
  </head>
  <body>
    <header>
      <h1>See the Flutter app below!</h1>
    </header>
    <div class="main-content">
      <main>
        <section class="interactive-iphone-display">
          <div id="flutter_host" style="height: 812px; width: 375px">
            Loading...
          </div>

          <div class="counter-controls">
            <button id="increase-btn">Increase</button>
            <div class="current-number">
              <label for="current-number-field">Current Number:</label>
              <input
                id="value"
                type="text"
                id="current-number-field"
                value="0"
              />
            </div>
          </div>
        </section>

        <section class="features">
          <h2>Features</h2>
          <ul>
            <li>Stunning Red Color Scheme</li>
            <li>Intuitive and Simple Counter Interface</li>
            <li>Revolutionary One-Tap Counting Technology</li>
          </ul>
        </section>

        <section class="download">
          <h2>Download Now</h2>
          <p>Join the counting revolution today. Available on all platforms.</p>
        </section>
      </main>
      <footer>
        <p>&copy; 2023 Red Counter. All Rights Reserved.</p>
      </footer>
    </div>
  </body>
</html>
```

　試しましょう🔥！

![1_J4ZMGFlpJh1S0vUHOl-l2g.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3522789/3a734410-2285-6633-fc3b-d0e18062b690.gif)

## 別のフレームワークとは?

- reactにとって、この作った例を見てください:  https://github.com/lucas-goldner/flutter_react_element_embedding

- angularもいい例があります: https://github.com/flutter/samples/tree/main/web_embedding/ng-flutter

- jasprとういうdartでウェブサイトが書けるフレームワークもエレメントエンべディングを
サポートしています: https://docs.page/schultek/jaspr/eco/flutter_embedding

質問があれば、Xで是非聞いてください！