NOTE
This tutorial is based on Flutter 3.24.4
I’ve been enjoying getting back into web programming after many years. I’m doing some web work in a contract role as well as working on this site.
I also have an idea for a project that I think will involve creating a web page with a Flutter application embedded in it. I’ve known this was possible for some time, having seen some demos for it a while back.
To start, I wanted to embed a “Hello Flutter” program into a static web page. I ran into some bumps along the way, and figured I’d share how to do this in somewhat of a tutorial form, as I think the official docs are somewhat lacking.
In the end, you’ll wind up being able to do something like this (not a screenshot, try clicking):
This is somewhat different (and more custom) than just deploying a Flutter app as a web page. Follow along below to learn how it’s done.
Step 1: Create a sample Flutter app
You can do this in an IDE like VSCode or from the command line, like so:
flutter create hello_flutter
That should create the classic Flutter sample app shown above with a counter that increments when you press a button.
Step 2: Customize the sample app for embedding
The sample app should run just fine as a standalone Flutter web app. Try something like this to launch it in a browser:
cd hello_flutter/
flutter run -d chrome
It won’t work to embed the sample app in a web page though. For that we’ll need
to create a helper widget that will adapt a widget like MyApp
to be embedded
in a web page and tweak how the app is started.
Let’s do the helper widget first. In this tutorial, I’m calling it WebGateway
.
Its purpose is to connect the lower-level
FlutterView
from the web platform to the regular goodness that you’re familiar with when
writing Flutter apps. This version handles a single child widget, but you can
check out the
multi_view_app.dart
sample in the embedding docs
if you’re interested in hosting multiple widgets from an app in separate containers
on a web page.
Copy the following code and paste it below the MyApp
code in lib/main.dart
.
class WebGateway extends StatefulWidget {
final Widget child;
const WebGateway({super.key, required this.child});
@override
State<WebGateway> createState() {
return _WebGatewayState();
}
}
class _WebGatewayState extends State<WebGateway> with WidgetsBindingObserver {
late Widget child;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_updateView();
}
@override
void didUpdateWidget(WebGateway oldWidget) {
super.didUpdateWidget(oldWidget);
_updateView();
}
@override
void didChangeMetrics() {
_updateView();
}
void _updateView() {
final flutterView = WidgetsBinding.instance.platformDispatcher.views.single;
setState(() {
child = View(view: flutterView, child: widget.child);
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return child;
}
}
Now, change the main
method to look like this:
void main() {
runWidget(const WebGateway(child: MyApp()));
}
In addition to wrapping MyApp
with WebGateway
, the other slight adjustment is
changing from runApp
to runWidget
. You can check out the
official embedding docs.
to learn why that’s necessary.
Step 3: Customize the JavaScript bootstrap code
We’re getting close! Let’s turn to the pure web side of the code now.
Unlike a standard Flutter web deployment where you might be hosting your full application at the root of a site, we’re going to put ours in a subdirectory since it’s going to be embedded as just one aspect of the site we’re going to deploy.
Still working in hello_flutter
, create a file called flutter_bootstrap.js
in the web
directory. Add the following contents.
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
config: {
entryPointBaseUrl: '/hello-flutter/'
},
onEntrypointLoaded: async function onEntrypointLoaded(engineInitializer) {
let engine = await engineInitializer.initializeEngine({
multiViewEnabled: true, // Enables embedded mode.
assetBase: '/hello-flutter/',
});
let app = await engine.runApp();
// Make this `app` object available to your JS app.
app.addView({
hostElement: document.querySelector('#flutter-element'),
});
}
});
A few key things to note about that code:
- We pass
multiViewEnabled: true
toinitializeEngine
. That’s required for Flutter 3.24+‘s new embedding mode. - We’re providing a custom path for
entryPointBaseUrl
andassetBase
1 arguments. That will let the app run properly hosted at/hello-flutter/
but embedded in another page in our site like/foo/index.html
. - We’re planning on hosting the app in a
div
element withid="flutter-element"
. That’s where the bootstrap code will attempt to hook up our app (viaaddView
).
At this point you should be able to build your Flutter app for the web and it will
generate all of the code required to deploy it in build/web
.
flutter build web
Step 4: Add the Flutter app to your site
Now it’s time to take the web code that was compiled in the last step and add it to a web site.
There are inummerable choices when it comes to web frameworks, servers, hosting, etc. If you’re experienced enough, you can probably adapt what I’m going to describe below to the framework/server/host of your choice.
Creating an Astro site
For this tutorial, I’m going to use Astro. If you have Node.js installed, you can create a new Astro site like this:
npm create astro@latest
It will prompt you with a few options to setup your project. Choose the following options:
Option | Selection |
---|---|
dir | ./hello_flutter_web |
tmpl | Empty |
ts | Yes |
use | Strict |
deps | Yes |
git | Yes |
Once it’s done creating the site, do the following:
cd hello_flutter_web
npm run dev
You should be able to open http://localhost:4321
in your browser and see
a plain looking website with an “Astro” header.
Adding the Flutter app to the home page
Open the index.astro
file (mine is at src/pages/index.astro
) and replace it
with the following content.
---
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
<style>
#flutter-element {
max-width: 600px;
width:80%;
margin:auto;
height:200px;
border-radius: 25px;
border: 2px solid #AAAAAA;
overflow: clip;
}
</style>
<script src="/hello-flutter/flutter_bootstrap.js" type="module" async></script>
</head>
<body>
<h1>Astro</h1>
<div id="flutter-element"/>
</body>
</html>
Back in a terminal in your hello_flutter_web
directory (or in your IDE), create
a new subdirectory called hello-flutter
in the public/
directory and copy
the compiled Flutter app into it.
mkdir public/hello-flutter
cp -R ../hello_flutter/build/web/* public/hello-flutter
Done! Flutter embedded in a web site
If your Astro site is still running with npm run dev
, you should see the page
reload and launch the Flutter counter sample app!
I hope this tutorial has been helpful, thanks for reading!
Troubleshooting
If the Flutter counter sample doesn’t load, check the following to see if you missed a step or made a typo along the way.
Astro logs [404] /main.dart.js
Verify that your hello_flutter/web/flutter_bootstrap.js
file has a config
with entryPointBaseUrl: '/hello-flutter/'
. Revisit Step 3 above if necessary.
Astro logs [404] /assets/FontManifest.json
Verify that your hello_flutter/web/flutter_bootstrap.js
file is passing
assetBase: '/hello-flutter/'
in the initializeEngine
call. Revisit
Step 3 above if necessary.
Bad state: No element
logged in browser console
If you see this error message and a long stack trace, you might have mismatched
HTML element IDs in your flutter_bootstrap.js
and index.astro
files. Revisit
Step 3 above if necessary.
Null check operator used on a null value
logged in browser console
If you see this error message and a long stack trace, you might have missed
wrapping MyApp
in the WebGateway
widget. See Step 2 above.
Rejecting promise with error: Null check operator used on a null value
If you see this error message and a long stack trace, you’re probably calling
runApp
instead of runWidget
. See Step 2 above.
Footnotes
-
The Flutter docs say that
assetBase
should be passed to the_flutter.loader.load()
call. That doesn’t work when following the Flutter web embedding instructions though; as of Flutter 3.24.4 it needs to be passed directly toinitializeEngine
. I’ve filed a bug for the Flutter team to track this issue. This bug was also part of the inspiration for my follow-up blog post on the Infinite Customization anti-pattern. ↩