Demonstrating bidirectional messaging between Flutter and a hosting web app

We're modifying the sample Flutter application to demonstrate bi-directional messaging between the HTML/Javascript and the Flutter code. The sample Flutter counter app is embedded in a web page. It communicates with Javascript via browser window messages.

  1. Demonstrates messaging Flutter to the web app via window messages whenever the counter increments.
  2. Demonstrates messaging from the web app to Flutter whenever a button is pressed on the HTML page. An "increment" message is passed to Flutter via the browser window messaging which causes the Flutter app to increment its counter.

Show me the code

Extracted snippets can be seen by scrolling down on this page.
  • https://github.com/freemansoft/flutter-embedded
  • https://github.com/freemansoft/flutter-embedded/tree/main/v1

Video on Joe's YouTube channel

The videos are always more expressive than anything I would write here.

Flutter Initiated message flow

Diagram used in the videos describing the action and message flow when one of the Flutter buttons is pressed. A counter is incremented by one in this flow.

A transcript will eventually be added here.

Javascript Initiated Message Flow

Diagram used in the videos describing the action and message flow when one of the HTML buttons is pressed. A counter is incremented by one in this flow.

A transcript will eventually be added here.

Index.html messaging code

The static site web app is a single HTML page with some JavaScript.  The payloads of all messages are JSON.

        window.addEventListener("message", handleMessage);

        // The flutter app posts to window and window parent if available
        // If running in iframe then message destined for parent is received here
        // If running not in iframe then parent and window are the same
        //    and message destined for parent AND window is received here
        // Using createTextNode which does not accept HTML markup
        function handleMessage(e) {
            var data = JSON.parse(e.data);
            // Only interested in messages with an "action":"incremented".
            // Ignore all the others
            // should check origin also
            if (data.action == "incremented"){
                console.log("javascript Message Received from origin: "+ e.origin + " data: " + e.data);
                var theDiv = document.getElementById("MESSAGE_RESULTS_DIV");
                var content = document.createTextNode(e.data);
                theDiv.appendChild(content);
            } else {
                // ignoring other messages
            }
        }


    <button id="button2" onclick="sendMessageWindowIFrame()">Increment flutter in iFrame</button>
    <br/;>
    <button id="button1" onclick="sendMessageWindow()">Increment flutter in window</button>

    <script>
        // sends a message to the flutter app in the element
        function sendMessageWindow() {
            const message = '{"action":"increment"}';
            // Should target the domain instead of '*'
            // Which one should we call out
            window.postMessage(message, '*');
        }
        function sendMessageWindowIFrame() {
            const message = '{"action":"increment"}';
            var flutterIFrame = document.getElementById('FLUTTER-IFRAME');
            // Should target the domain instead of '*'
            // Which one should we call out
            flutterIFrame.contentWindow.postMessage(message, '*');
        }
    </script>

Dart messaging code

The Flutter app is a single dart file.  The payloads of all messages are JSON.

Dart receiving messages

      html.window.onMessage.listen(
        (html.MessageEvent event) => _listen(event),
      );

Note that the dart code drops all messages other than the "increment" command

  /// Called when a window message event is received
  void _listen(html.MessageEvent event) {
    final eventData = event.data;
    // only increment if we receive the increment command
    // should verify is string and ignore if not
    // debugPrint('${eventData.runtimeType}');
    final data = jsonDecode(eventData as String);
    // Should check origin also
    if (data['action'] == 'increment') {
      debugPrint(
          'Flutter received from origin: ${event.origin} data: $eventData');
      _incrementCounter();
    } else {
      // Receive all window messages for that window and domain
      // Ignore the onces we aren't interested in
    }
    // _incrementCounter calls setState
    //setState(() {});
  }


Dart sending messages

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
      _postActionMessage(
          action: "incremented", actionPayload: {"count": _counter});
    });
  }

The code in the repository has additional logic to determine if the Dart code is running in a web app or mobile native.  There is a bunch of learning-related logic here with the window and the window.parent.  A real app would use only one.

/// @param actionPayload can be antything.  we use it for package info
void _postActionMessageWeb(
    {required String action, required Map<String, dynamic> actionPayload}) {
  Map<String, dynamic> data = {"action": action};
  data.addAll(actionPayload);

  // use html.window.parent if we are in an iframe
  if (html.window.parent != null) {
    data["location"] = html.window.parent!.location.toString();
    final json = const JsonEncoder().convert(data);
    debugPrint('flutter Message fired: $json to parent.');
    html.window.parent!.postMessage(json, "*");
  }

  // if there is no parent or the parent and window are different
  // This will be true if in iframe but no one will receive it
  if (html.window.parent == null ||
      html.window.parent!.location.toString() !=
          html.window.location.toString()) {
    data["location"] = html.window.location.toString();
    final json = const JsonEncoder().convert(data);
    debugPrint('flutter Message fired: $json to window.');
    // should target a domain instead of global
    html.window.postMessage(json, "*");
  }
}

Video

Demonstrating bidirectional messaging between Flutter and a hosting web app

Showing the code for bidirectional messaging between Flutter and a hosting web app

Revision History

Created 2024 05

Comments

Popular posts from this blog

Understanding your WSL2 RAM and swap - Changing the default 50%-25%

Installing the RNDIS driver on Windows 11 to use USB Raspberry Pi as network attached

DNS for Azure Point to Site (P2S) VPN - getting the internal IPs