How To Create a Communication Bridge Between Flutter And JavaScript

Written by tomerpacific | Published 2020/12/06
Tech Story Tags: flutter | flutter-webview | communication | javascript | tech | backend | application | android

TLDR Flutter does NOT have built in support for embedded WebViews. Unlike a native application in either Kotlin or Swift, you cannot just add a WebView component to your application out of the box. All local assets in a Flutter application need to reside inside an assets directory in your main project hierarchy by right clicking in the left side panel and choosing New → Directory. Create a new Flutter project, use the webview_flutter package to be able to use WebView. Add the dependency to our pubspec.yaml.via the TL;DR App

As a follow up to my article explaining how to create communication bridges in Android and iOS, I thought it might be a good idea to do the same for Flutter. While it may seem like this is a straightforward affair, you’ll soon realize it takes a bit of work to get this functionality working.
First and foremost, it is important to realize that (at the time of writing this article) Flutter does NOT have built in support for embedded WebViews. Meaning, that unlike a native application in either Kotlin or Swift, where you can just instantiate a WebView component, you cannot just add a WebView component to your application out of the box.
After creating a new Flutter project, we need to use the webview_flutter package to be able to use a WebView. We will add the dependency to our pubspec.yaml file:
dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^1.0.7
Then, we need to run Pub get or in the terminal:
flutter pub get
Next, we need to import the package in our main.dart file:
import 'package:webview_flutter/webview_flutter.dart';
If you haven’t cleaned up the code from the starter project yet, now is a good time to do so. After you remove all the comments, the floating action button and everything related to it, you will be left with this (I added a text widget just for show):
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Communication Bridge',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Native - JS Communication Bridge'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  WebViewController _controller;

  @override
  Widget build(BuildContext context) {
    return Text(
      "Flutter JS-Native Communication Bridge"
    );
  }
}
Which will give you this result:

Adding Our Local File

Since we will be using a local html file with embedded JavaScript code inside of it, we need to create it in our project. All local assets in a Flutter application need to reside inside an assets directory. Create an assets directory in your main project hierarchy by right clicking in the left side panel and choosing New → Directory. This directory needs to be a sibling of the android directory.
Then, go on ahead and create index.html inside the assets directory.
<html>

    <head>
        <title>My Local HTML File</title>
    </head>

    <body>
        <h1 id="title">Hello World!</h1>
        <script type="text/javascript">
            function fromFlutter(newTitle) {
                document.getElementById("title").innerHTML = newTitle;
                sendBack();
             }

             function sendBack() {
                messageHandler.postMessage("Hello from JS");
             }
        </script>
    </body>
</html>
You will notice that we have written two methods in the JavaScript section of our html:
  1. fromFlutter - is the method we will call from Flutter with a string representing the new title for the page
  2. sendBack - is the method we will call to communicate back to Flutter. In it we are sending a string message.
We will get into the contents of sendBack in a minute, but before that, we have to set up our WebView in our application.
✋ Don’t forget to add index.html to your pubspec.yaml under an assets section (use the correct indentation)
dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^1.0.7
  cupertino_icons: ^1.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
    
flutter:
  uses-material-design: true
  
  assets:
    - assets/index.html

Setting Up The WebView

Since we already imported the package into our main.dart file, we need to replace the Text widget with a WebView widget.
class _MyHomePageState extends State<MyHomePage> {

  WebViewController _controller;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Webview')),
      body: WebView(
        initialUrl: 'about:blank',
        onWebViewCreated: (WebViewController webviewController) {
          _controller = webviewController;
          _loadHtmlFromAssets();
        },
      ),
    );
  }

  _loadHtmlFromAssets() async {
    String file = await rootBundle.loadString('assets/index.html');
    _controller.loadUrl(Uri.dataFromString(
        file,
        mimeType: 'text/html',
        encoding: Encoding.getByName('utf-8')).toString());
  }

}
We wrapped our WebView with a Scaffold widget(it’s purpose will be revealed later in the article), but let’s focus on the different fields of the WebView widget seen above:
  • initialUrl - is where we can define which url the WebView points to. Here we decided to point it to nothing since we are going to load our local html file
  • onWebViewCreated - is a callback we get from the package once the WebView is created. Since we want to save the controller instance that we get from this callback, we have created a private member to store it to(_controller)
You will also notice that we created a method called _loadHtmlFromAssets, which as is implied by it’s name, will load our local html file into the WebView.
Inside this method, we use our private WebViewController instance, _controller, and it’s exposed method loadUrl to load our local html file. Due to the logic in this method, it’s execution is asynchronous.
If we run our application, we will get the following:

Let’s Communicate (Flutter -> WebView)

Now let’s add some functionality to call the fromFlutter method we defined in our local html file. To do that, we will be adding a Floating Action Button (or FAB) to our layout and connecting it’s onPressed method to call the fromFlutter method. That is also the reason behind the usage of the Scaffold widget, so we can easily add a FAB.
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Webview')),
      body: WebView(
        initialUrl: 'about:blank',
        javascriptMode: JavascriptMode.unrestricted,
        onWebViewCreated: (WebViewController webviewController) {
          _controller = webviewController;
          _loadHtmlFromAssets();
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.arrow_upward),
        onPressed: () {
          _controller.evaluateJavascript('fromFlutter("From Flutter")');
        },
      ),
    );
  }
In order to make calls from Flutter to our loaded html we are using the evaluateJavascript method. To be able to use it, we must add another property to our WebView, called javascriptMode. Above, we are setting it to unrestricted. If we don’t set it, we will not be able to communicate between Flutter and the WebView.

Communicate Back (WebView -> Flutter)

Remember how I said we will talk about the contents of our sendBack method? Well, now is the time we do that.
function sendBack() {
  messageHandler.postMessage("Hello from JS");
}
In the sendBack method we are using an object called messageHandler and it’s attached method called postMessage. If you have ever created a communication bridge in a native application, then you are aware that once you set one up, you are adding an object to the global window object in the Javascript layer to be used for communication. You can name this object to whatever you like as long as you reference it when you make calls from Javascript to your native application.
How is this object added to the Javascript layer in our application? By adding the JavascriptChannels attribute to our WebView widget:
class _MyHomePageState extends State<MyHomePage> {

  WebViewController _controller;
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: Text('Webview')),
      body: WebView(
        initialUrl: 'about:blank',
        javascriptMode: JavascriptMode.unrestricted,
        javascriptChannels: Set.from([
          JavascriptChannel(
              name: 'messageHandler',
              onMessageReceived: (JavascriptMessage message) {
               _scaffoldKey.currentState.showSnackBar(
                  SnackBar(
                      content: Text(message)
                  )
                 );
              })
        ]),
        onWebViewCreated: (WebViewController webviewController) {
          _controller = webviewController;
          _loadHtmlFromAssets();
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.arrow_upward),
        onPressed: () {
          _controller.evaluateJavascript('fromFlutter("From Flutter")');
        },
      ),
    );

  }
We have defined a JavascriptChannel with a name and an onMessageReceived handler. The name we have given this channel, messageHandler, is the name we are using to communicate from the local html file we loaded to our native layer.
For the keen eyed, you probably noticed that a new private variable has been added, _scaffoldKey. This is because we needed to add a key to our Scaffold widget so we can display the Snackbar.
You can get the source code for the application described in this article here.
Two final points to be aware of:
  1. The alert method is broken in the webview_flutter package
  2. To use the package in iOS, you must add the following key to your info.plist file:
  3. <key>io.flutter.embedded_views_preview</key><string>yes</string>
Other sources you may find helpful if you want to learn more about Flutter and WebViews:

Published by HackerNoon on 2020/12/06