Flutter Element Embedding — Unleashing the Power of Integrating Flutter Apps into Websites, including React-powered ones!

Lucas Goldner
8 min readApr 23, 2024

When I first learned about Element Embedding at FlutterForward 2023, its potential immediately captivated me.

Imagine showcasing a vertical slice of your app directly on your homepage, allowing users to interact with it and truly experience its functionality. But that’s just the start. You can also embed multiple smaller apps within your documentation to demonstrate their behavior in real-time

Capabilities of Flutter Element Embedding:

  • Embedd flutter web-compatible apps into any website
  • Using JS Interop, it is possible to share the state of the app and the website and modify it from each other
The actual Flutter app is rendered on the home page of the webpage. https://flutter-show-docs.vercel.app/

How Can You Implement Flutter Element Embedding?

Achieving this is relatively straightforward. First, I’ll demonstrate how to embed Flutter elements on a simple HTML website for demonstration purposes. If you’re interested in viewing the entire source code, you can refer to the repository provided below:

But we will be starting from scratch:

<!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>
Starting page

1. We need a web build of our Flutter app. So, let us run the build command:

$ flutter build web

2. Once complete, you must add the following two scripts to your website. Both scripts can be found in the build directory of your Flutter web app. When properly configured, flutter.js manages the loading of the web app and Flutter's service worker.

<script src="./your_built_flutter_app/flutter.js" defer></script>

This snippet, which loads the Flutter app by reading main.dart.js, is also essential. There are numerous parameters available to customize and configure the engine initialization. For more detailed information on these parameters, please visit Flutter Web Initialization Documentation.

The most crucial parameters to consider are the assets and the hostElement, as these dictate where the Flutter app will be embedded on the website.

    <script>
window.addEventListener("load", function (ev) {
const basePath = "./your_built_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>

Adding both of those results to this updated website:

<!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>

And that is it 🎇! Enjoy the result below:

But I use React. How can I integrate it there?

I encountered some issues when trying to apply the steps I had learned. Fortunately, I managed to figure it out with the help of a user’s guide. I will explain the process in the following step

1. Run this command in your Flutter app to create a web version:

$ flutter build web --profile --dart-define=Dart2jsOptimization=O0

This command will not generate a minified JavaScript version because some adjustments need to be made in the build output. As a result, the client will have to download a larger bundle.

2. Copy the web folder from /build/web/ to the public folder in your React app. In this project, I have renamed the folder to Flutter. All our Flutter files are now hosted at YOUR_HOST/flutter/.

Navigate to public/flutter/main.dart.js. In the next step, replace the t1 path with your folder name.

// what you will need to find
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;
}

// replaced
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. Edit flutter.js

// Search for this
function getBaseURI() {
const base = document.querySelector("base");
return (base && base.getAttribute("href")) || "";
}

// Use the same path as before
function getBaseURI() {
return "/flutter/";
}

4. Create a flutter_init.js file inside your public folder of the react app and paste the following content into it:

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. Install react-helmet-async so we can run our flutter_init script

$ yarn add react-helmet-async

6. The setup is done now. You need to wrap your React App with a HelmetProvider and provide a helmetContext (can be empty).

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. Now that that is resolved, add a Helmet component and our flutter_init.js + flutter.js. This will load the Flutter app, and that was it!

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>
</>
);
}

That was it! Now, your Flutter app should be displayed in React as well!

The process can be cumbersome, so I also wrote a small script to simplify it, as some steps need to be repeated with each build. Feel free to copy and adjust this script for your purposes.

Now, let’s make the interaction between the website and the app more dynamic by allowing them to manipulate each other

The next goal is to enhance the website by adding elements that can read and display the app's state. In our example, I've added a text field designed to show the current number from the app and an "Increase" button to modify this number. Here’s a reference to how it looks:

To implement this interaction, it is essential to facilitate communication with JavaScript. This can be achieved using JavaScript Interoperability (Js Interop), which allows the Flutter app and the website to interact seamlessly.

1. First, add the js package

$ flutter build web

2. This step will vary depending on your app and its specific requirements. For instance, within your Flutter app, you should export all functions and states that you want to be accessible via JavaScript. Here is an example of how you might configure this:

In this scenario, we use the @js.JSExport() annotation for the state, which needs to be assigned using the js_util library's setProperty function. This setup maps the state to a string, making it accessible from the JavaScript side. Additionally, the callMethod function allows you to invoke JavaScript functions from Flutter. It also supports passing additional values in its array, providing further flexibility for interaction between the Flutter app and the website.

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

@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', []);
}
....
}

For any function or variable you want to make accessible from JavaScript, you need to mark it with the same decorator, like so:

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

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

The reason for using a stream controller in a straightforward app like Flutter is to facilitate sending “updates” to the website’s text field, which we plan to add. This method ensures that any changes in the app can be dynamically reflected on the website, enhancing interactivity and user experience.

3. Now that everything is configured on the Dart side, it’s time to move forward. Next, we’ll bind our exported functions and states to variables. To keep the logic organized, I’ve created a new main.js file where I manage these assignments:

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

And in our index.html, I load with this script:

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

So, the final result of our code becomes:

<!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>

Let us try it out🔥

Now that we have established direct communication, you can change the app’s state using the website and vice versa. This opens up a wealth of new possibilities for interaction and functionality. If you have any impressive examples or innovative uses, please share them with me on X.

The code is available here if you want to look something up:

Other frameworks?

…And that was it. Please let me know if you have any questions. I am happy to help 😄

--

--

Lucas Goldner

Student at HdM in Stuttgart, Germany. My passion lies in mobile development, but I also like web development. Especially enjoy Flutter and iOS