Embedding Flutter on the Web

Updated on
Embedding Flutter on the Web

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:

  1. We pass multiViewEnabled: true to initializeEngine. That’s required for Flutter 3.24+‘s new embedding mode.
  2. We’re providing a custom path for entryPointBaseUrl and assetBase1 arguments. That will let the app run properly hosted at /hello-flutter/ but embedded in another page in our site like /foo/index.html.
  3. We’re planning on hosting the app in a div element with id="flutter-element". That’s where the bootstrap code will attempt to hook up our app (via addView).

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:

OptionSelection
dir./hello_flutter_web
tmplEmpty
tsYes
useStrict
depsYes
gitYes

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!

A screenshot showing a Flutter app embedded in a web page

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

  1. 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 to initializeEngine. 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.