「Flutterforward 2023」のイベント祭に初めて「エレメントエンべディング」を発表された、そういう機能ができて、驚きました。
早速色んな使い方を思い浮かびました、例えばホームページ上でユーザーがアプリを直接試すことができます。もう一つのアイデアとしてドキュメンテーションのページで実際の例が見られます。
Flutter エレメントエンべディングのメリットは:
- 実際のアプリを直接使用することができるんです
- アプリとウェブサイト間で情報を共有することが出来ます
この例はこちらで見られます
実装方法
実は実装方法は簡単です。まず、普通の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>
- アプリをウェブバージョンビルドをしないといけません
$ flutter build web
- 次のステップはウェブサイトにスクリプトをロードします。まず、flutterのエンジンスクリプトとロードを追加します。flutter.jsはflutterのエンジンの開始コードが入っています
<script src="./あなたのflutterアプリ/flutter.js" defer></script>
エンジンの「ロード」イベント時に別のスクリプトで自分のアプリをロード出来ます。これを使って、ウェブサイト内にアプリを入れます。エンジンは色んな設定がありますので、こちらで確認してください
<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>
ウェブサイトコードに追加したら、この結果をもらえます
<!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>
これで全部です!その方法とおりに、実装したら、これが見られます:
Reactを使ったらどうする
前の述べたらようにReactウェブサイトで実装したら、問題を見つけました。このガイドのおかげで、結局にエンべディングができました。
1: まずこのコマンドでビルドを書かないといけません
$ flutter build web --profile --dart-define=Dart2jsOptimization=O0
縮小を点けませんでしたので、このアウトプットは普通の比べて、サイズより大きい。その理由で依頼人は大きいバンドルをダウンロドをしないと
2: webファイルは/build/web/
からreactのpublic
ファイルにコピーしてください。このプロジェクトではweb
ファイルはflutter
に変えましたので、全部はあなたのホスト/flutter/
からアクセスことができます.次にpublic/flutter/main.dart.js
を開けて、t1
は自分のpathで変えないといけません
// これを探して
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
に変更して
// これを探して
function getBaseURI() {
const base = document.querySelector("base");
return (base && base.getAttribute("href")) || "";
}
// 同じpathでこちらに変更しないと
function getBaseURI() {
return "/flutter/";
}
4: publicファイルで新しいflutter_init.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を追加して
$ yarn add react-helmet-async
6: 準備が終わりました!ReactのアプリはHelmetProviderで包んで
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のアプリが見られます
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に使ってください!
#!/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 ✨"
ウェブサイトからアプリの状態を変更してみましょう
この例では、ウェブサイトにボタンとテキストフィルドを使いしました
ウェブサイトからアプリの状態を変更するために、「JS Interop」を使います。まず、Dartコードをウェブサイトからアクセスできるように設定します。
1:「js」パッケージを追加します
$ flutter build web
1:「js」パッケージを追加します
$ flutter build web
2: dartとjsのコミュニケーション
アプリと仕様によって、このステップは異なっています。ですから、このリポジトリでは例が見られます。Decoratorパターンを使ってアプリの状態と関数をエクスポートします。
@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」関数をウェブサイトのコードで使用できるようにします。
@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のコントロラは必要がありませんけど、この場合は、カウンタを変わるたびに、ウェブサイトのテキストフィルドを変更したいです
- dartのコードはjsにアサインをします
ウェブサイトのコードを見てみましょう。まず、「_stateSet」関数を設定します。この例では、プリント機能のために使っています。次に、ブラウザのグローバル「window」オブジェクトに「_appState」を追加します。残りのコードでは、入力フィールドとDartの関数を組み合わせます
// 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作りました
// 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();
});
};
})();
そのスクリプトをロドします
<script src="./main.js" defer></script>
最後の結果はこれになります
<!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>© 2023 Red Counter. All Rights Reserved.</p>
</footer>
</div>
</body>
</html>
試しましょう🔥!
別のフレームワークとは?
-
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で是非聞いてください!