We will cover briefly:

  1. What’s Element Embedding
  2. Embed Counter App in HTML webpage

What’s Element Embedding

The Flutter Forward event, which was broadcasted from Nairobi, Kenya, discussed Flutter’s future plans.

The team presented a sneak peek at the upcoming wave of investments in Flutter at this event, including early support for cutting-edge graphics performance, seamless integration for web and mobile, and early support for new and emerging architectures.

One such feature the team previewed was Element Embedding. This allows Flutter content to be added to any standard web <div> When integrated in this manner, Flutter merely turns into a web component, integrating seamlessly with the web DOM and even allowing the styling of the parent Flutter object with CSS selectors and transformations.

Proof of Concept

This is a proof-of-concept demo that was shown at Flutter Forward, you can see a simple Flutter app embedded in an HTML-based webpage. The demo also showed how to make changes to the Flutter state using a JavaScript event handler and an HTML button.

Demo at Flutter Forward Event
Demo at Flutter Forward Event

Embed Counter App in HTML webpage

We will embed the famous Counter App inside an HTML-based webpage.

Pre-Requisite

  • We should be using the flutter master channel
  • The Dart version should be 3.0.0 or above

Let’s create a project using

flutter create web_embedding --platforms web

Note: We specify the platform to be web meaning the project only supports web

By default, Flutter gives us the counter app using the template above.

We include the js package in our project to enable seamless interoperability between JavaScript and Dart code. Any function in your Dart code can be annotated with a @JSExportproperty using js, allowing you to invoke it from JavaScript code.

flutter pub add js

Changes to the Counter App

Our project has only 2 dart files as shown below

Dart Files
Dart Files

We refactor the main.dart take all the counter logic and put it inside the counter.dart This contains the MyHomePage which is a stateful widget (as we need to maintain the counter state) and _MyHomePageState is the state class for the MyHomePage widget

class MyHomePage extends StatefulWidget {
  
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

@js.JSExport()
class _MyHomePageState extends State<MyHomePage> {}

Next, we import the package js and js_util as

import 'package:js/js.dart' as js;
import 'package:js/js_util.dart' as js_util;

The _MyHomePageState is annotated with the @JSExportattribute since we also need to change the counter state from the JavaScript side. This makes the Dart object _MyHomePageState as exportable

Inside our initState we call the createDartExport for the _MyHomePageState which creates a JS object literal that forwards to our exported Dart class (which in our case is _MyHomePageState)

The createDartExport() method is used to create a JavaScript object that has a reference to a Dart object, which can then be accessed from JavaScript code. The setProperty() and callMethod() methods are used to set up JavaScript properties and call JavaScript functions, respectively.

void initState() {
  super.initState();
  final export = js_util.createDartExport(this);
  
  // These two are used inside the [js/js-interop.js]
  js_util.setProperty(js_util.globalThis, '_appState', export);
  js_util.callMethod<void>(js_util.globalThis, '_stateSet', []);
}

A JsObject instance export is created by calling the createDartExport() method provided by the js_util library. This creates a JavaScript object that has a reference to the Dart object that is being exported.

Two JavaScript properties are set using the setProperty() method provided by js_util. The first property is _appState, which is set to the export object created in the previous step. This property can be accessed from JavaScript code to interact with the Dart object.

The second property is _stateSet, which is a JavaScript function that is defined in another file (js-interop.js). This function is called using the callMethod() method provided by js_util. The empty array [] passed as the second argument indicates that no arguments are being passed to the function.

Note: _appState and _stateSet are present inside the js file, which we will discuss now.

We create a file js-interop.js inside our project under the folder web/js Inside the index.html we import this file under the scripts section as

<script src="js/js-interop.js" defer></script>

The src attribute specifies the URL of the JavaScript file to be loaded, in this case js-interop.js. The defer attribute tells the browser to defer the execution of the script until after the document has been parsed, which can improve page load times and prevent blocking of other resources.

JavaScript File
JavaScript File

All the interop with Dart is present inside this file. We make use of IIFE to create a function

An Immediately-invoked Function Expression (IIFE) is a way to execute functions immediately, as soon as they are created. IIFEs are useful as they don’t pollute the global object, and provide a simple way to isolate variables declarations

(function () {
  window._stateSet = function () {
    console.log('HELLO From Flutter!!')
  };
}());

This code sets the _stateSet function as a property of the global window object. It is intended to be called by Flutter when it prepares the JS-interop. When called, the _stateSet function will log the message “HELLO From Flutter!!” to the browser console.

Next, we access the _appState and save it inside a variable inside the JS

let appState = window._appState;

We create an HTML file called index.html and inside it create buttons using HTML tags. And since we want to show the counter app, we also create a div with id as flutter_target

<section class="contents">
    <aside id="demo_controls">
      <fieldset id="interop">
        <legend>JS Interop</legend>

        <!--This is the value box-->
        <label for="value">
          Value
          <input id="value" value="" type="text" readonly />
        </label>

        <!--This is the button-->
        <input id="increment" value="Increment" type="button" />
      </fieldset>
    </aside>

    <!--This is the div which contains the flutter app-->
    <article>
      <div id="flutter_target" class="center"></div>
    </article>
</section>

If we run the app at this point, we see this

Buttons in Web
Buttons in Web

Notice, we don’t see our Flutter Counter App, although we created a div for us to show the Flutter App.

We tweak the script tag inside the index.html by first adding an event listener to the window Get the div id, which represents the flutter app in our case flutter_target, using the query selector

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();
    },
  });
});
  • Using the _flutter. loader JavaScript API offered by flutter.js, you can modify how a Flutter app is launched on the web.

The following phases make up the initialization process:

  • Entry point script is loaded, the service worker is initialized after retrieving the main.dart.js script.
  • Initializing the Flutter engine which downloads the necessary files, including CanvasKit, fonts, and assets, to launch the Flutter web engine.
  • Running the application which executes your Flutter app after preparing the DOM for it.

Once the Service Worker is started and the browser has downloaded and executed the main.dart.js entry point, the loadEntrypoint method invokes the onEntrypointLoaded callback.

The onEntrypointLoaded the callback receives an engine initializer object as its only parameter. We use the engine initializer to set the run-time configuration and start the Flutter Web engine.

The initializeEngine() the function produces a Promise that resolves to an app runner object. The Flutter app is launched by the app runner’s single method, runApp()

And if the run the app now, we are able to the Flutter Counter App

Flutter Counter App in HTML
Flutter Counter App in HTML

Interoperability between Javascript and Dart

Currently, neither the floating action button nor the increment html button will update the contents of the JS text field in the flutter app when they are clicked.

Let’s fix this

To increase the counter value, we create the increment function inside of the counter.dart file and annotate it with the @JSExport property (making it accessible from the JS side)

@js.JSExport()
void increment() {
  setState(() {
    _counterScreenCount++;
  });
}

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

We tweak our counter variable and create a getter called count that holds the current value of the screen count. This getter is then annotated with JSExport thus making it accessible from JS side.

JS Changes

The snippet first uses the document.querySelector() method to find the HTML input element with the id of value, and assign it to the variable valueField.

// Get the input box i.e `value`
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();

Next, we define a function called updateState, which sets the value property of the input element to the value of appState.count. The updateState function is not called directly, but is instead registered as a callback using the appState.addHandler() method. This means that the updateState function will be called every time appState changes.

appState (note: this is the variable that holds the state of the flutter app)

Note: We need to create addHandler function

The updateState function is responsible for updating the HTML input element to reflect changes in the Flutter application state.

And because the counter’s initial value is 0, we use the function updateState to display 0 in our html input text box.

addHandler

Let’s create this function inside the counter.dart

A StreamController instance _streamController is defined with the type StreamController<void>.broadcast(). The broadcast() method creates a stream controller that can handle multiple subscribers.

The @JSExport() decorator is used to indicate that the addHandler() method should be exported to JavaScript code.

final _streamController = StreamController<void>.broadcast();

@js.JSExport()
void addHandler(void Function() handler) {
  // This registers the handler we wrote in [js/js-interop.js]
  _streamController.stream.listen((event) {
    handler();
  });
}

The addHandler() method is defined to take a single argument handler that is of type void Function().

Inside the method, a StreamSubscription is created by calling the listen() method on _streamController.stream. The listen() method takes a callback function as an argument, which will be called each time an event is added to the stream.

The handler()function, which was passed as a parameter, is invoked inside the callback function. This method can be called from JavaScript code to register a callback function that will be invoked whenever an event is added to the stream.

Next, we modify our existing functions and add an event to the streamController to make sure the handler we wrote in the js-interop.js gets invoked.

Inside the setState() method, the _counterScreenCount value is incremented by 1, and then the StreamController instance _streamController is notified of the change by adding a null value to the stream using _streamController.add(null).

@js.JSExport()
void increment() {
  setState(() {
    _counterScreenCount++;

    // This line makes sure the handler gets invoked
    _streamController.add(null);
  });
}

And if the run the app now, we are able to the see the input box with default value as 0

JS Default value
JS Default value

If we click on the increment button, we see the values don’t reflect in the flutter side and vice-versa.

Sending events between JS and Dart

let incrementButton = document.querySelector("#increment");

incrementButton.addEventListener("click", (event) => {
  appState.increment();   // Function present inside the `client.dart`
});

We get a reference to the HTML button element with the ID incrementusing the document.querySelector() method. This method returns the first element that matches the specified selector.

The addEventListener() method is then called on the button element to register an event listener for the “click” event.

The second argument to addEventListener() is an arrow function that is executed when the click event is fired. Inside this arrow function, the appState.increment() function is called. This function is defined in the code (in client.dart) and is responsible for incrementing value in the application state.

This increment function is present inside the counter.dart which simply increases the counter value in the dart side, by adding an event to the stream controller.

_streamController.add(null);

When an event is added to the stream controller, the addHandler method, which calls the callback set up in the JS , is also fired.

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

In this manner, the UI is updated for both whenever the increment button is pressed, whether from the Dart side or the JS side.

Source Code

Valuable comments