Writing a Flutter Web plugin with JavaScript: a step by step guide

image

Nikita Shuliak

image

Introduction

If you have an idea of either creating or adding web support for a Flutter plugin, but you don’t have enough experience with JavaScript or Flutter Web, then this article is specifically intended for you. We will talk about how to call a JavaScript code from Dart, how to handle JavaScript calls on the Dart side, as well as about the features in writing such a code for a Flutter web plugin in JavaScript and Dart parts.

Communication between Dart and JavaScript

First of all we need to understand how we can pass data between Flutter and the Web. On Android and iOS platforms we use MethodChannel to invoke native parts from a Flutter app, but on the Web to make your Dart code capable of communicating with JavaScript and vice versa we need to use the official core package called js. It provides a way to declare methods that can be called from Dart and JavaScript code.
In your Flutter plugin project add js dependency into the pubspec.yaml file (the changes are highlighted):

flutter_plugin/pubspec.yaml

...
dependencies:
 flutter:
   sdk: flutter
 js: 0.6.3
...

To call JavaScript code from Dart, declare a global function in the JavaScript code and then create an external function with @JS() annotation in the Dart code.

JavaScript code example:
flutter_project/web/index.js

//Declares a global function
window.jsInvokeMethod = async (method, params) => {
   //Process your Dart call here
}

Dart code example:
flutter_plugin/lib/flutter_plugin_web.dart

@JS()
library callable_function;
 
import 'package:js/js.dart';
 
@JS()
external dynamic jsInvokeMethod(String method, String? params);

Finally, let’s call the JavaScript method from Dart.
flutter_plugin/lib/flutter_plugin_web.dart

dynamic sendMethodMessage(
   String method, String? arguments) {
 final dynamic response = jsInvokeMethod(method, arguments);
 //...
}

But most likely, if you add support for some native JavaScript library, you will face JavaScript asynchronous methods. In such a case to convert JavaScript Promise into the Dart Future, import package:js/js_util.dart and use promiseToFuture method.

Handle the JavaScript Promise Dart code example:
flutter_plugin/lib/flutter_plugin_web.dart

//...
import 'package:js/js_util.dart';
//...
 
Future<dynamic> sendMethodMessage(
   String method, String? arguments) async {
 final dynamic response =
     await promiseToFuture(jsInvokeMethod(method, arguments));
 //...
}

Now let’s call a Dart code from JavaScript. For that, we also need to declare a function with @JS named annotation and then create a function that can be called from JavaScript with allowInterop method from package:js/js.dart. Pass to the allowInterop method a callback function which will be triggered each and every time after JavaScript calls a jsOnEvent function.

Dart code example:
flutter_plugin/lib/flutter_plugin_web.dart

//..
@JS('jsOnEvent')
external set _jsOnEvent(void Function(dynamic event) f);
//..
 
//..
//Sets the call from JavaScript handler
_jsOnEvent = allowInterop((dynamic event) {
 //Process your JavaScript call here
});
//..

Now let’s call Dart from JavaScript.
flutter_project/web/index.js

window.jsOnEvent('Hello from JavaScript')

This is it, let’s move ahead and prepare the JavaScript code for the plugin.

JavaScript part of the Flutter plugin

The first thing to understand is that there is no need to create any folders with JavaScript code in your Flutter plugin. All JavaScript code will be either placed or imported directly in your web folder in the Flutter project.
First of all, create the index.js file in your Flutter project web folder. The code in the index.js will look something like this.

flutter_project/web/index.js

//Your JS libraries imports
//...
 
//Declares a global function
window.jsInvokeMethod = async (method, params) => {
   //Process your Dart call here
}
 
//Sends event to Dart
function callDart(event) {
   window.jsOnEvent(event)
}
 
//Your JS code
//...

Then create a package.json file in the web folder and put the code below there.

flutter_project/web/index.js

{
 "name": "web",
 "version": "1.0.0",
 "main": "index.js",
 "scripts": {
   "build": "webpack --config webpack.config.js"
 },
 "dependencies": {
   "webpack": "5.28.0",
   "webpack-dev-server": "3.11.2",
   "webpack-cli": "4.5.0"
 }
}

Finally create a webpack.config.js file in the web folder and put this code there.

flutter_project/web/index.js

{
 "name": "web",
 "version": "1.0.0",
 "main": "index.js",
 "scripts": {
   "build": "webpack --config webpack.config.js"
 },
 "dependencies": {
   "webpack": "5.28.0",
   "webpack-dev-server": "3.11.2",
   "webpack-cli": "4.5.0"
 }
}

Install the npm if you previously did not. We will use webpack to generate the final JavaScript bundle file which we will deploy and connect to the index.html later.
Finally, create a webpack.config.js file in the web folder and put this code there.

Now navigate to the project web folder and run the npm install command:

$ cd ~/web
$ npm install

After that bundle your JavaScript files using npm run build command:

$ npm run build

The bundle.js file appears in the dist folder, which you can specify in the index.html file.

Let’s add it to the bottom of the head tag:
flutter_project/web/index.html

<!DOCTYPE html>
<html>
<head>
 <!--...-->
 <script type="text/javascript" src="./dist/bundle.js"></script>
</head>
<body>
 <!--...-->
</body>
</html>

Now you can call your JavaScript code from Dart, but let’s go a little further and deploy the bundle file to use it by HTTP link.

JavaScript bundle file deployment

To make your bundle file accessible by HTTP link, put it in the GitHub repository and use jsdelivr CDN to generate a jsDelivr link from your GitHub bundle file link.

Migrating from GitHyb to jsDelivr

Put this link instead of the local link in the index.html file.

flutter_project/web/index.html

<!DOCTYPE html>
<html>
<head>
 <!--...-->
 <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/NikitaPipita/flutter_web_plugin_example@master/web/bundle/bundle.js"></script>
</head>
<body>
 <!--...-->
</body>
</html>

Congratulations! Now your JavaScript bundle is accessible to use by the link.

Dart part of the Flutter plugin

Now let’s move to the Flutter plugin part. Add the following dependencies to the pubspec.yaml file (new changes are highlighted):

flutter_plugin/pubspec.yaml

...
dependencies:
 flutter:
   sdk: flutter
 flutter_web_plugins:
   sdk: flutter
js: 0.6.3
...

Also specify a Flutter plugin web entry point class and a path to it in the pubspec.yaml.

flutter_plugin/pubspec.yaml

flutter:
 plugin:
   platforms:
     android:
       package: com.example.flutter_plugin
       pluginClass: FlutterPlugin
     ios:
       pluginClass: FlutterPlugin
  web:
       pluginClass: FlutterPluginWeb
       fileName: flutter_plugin_web.dart

Let’s move on and code the web entry point class. Like on iOS and Android, the web part of the plugin also needs to be registered before being used. This is done in a static registerWith method in the web plugin class, which will be called when your web app starts.
To make your web support more compatible with previously added Android and iOS plugin realizations, let’s register previously used ones for communication with native parts MethodChannels here, in registerWith method, using Registrar from package:flutter_web_plugins/flutter_web_plugins.dart. Now all MethodChannels calls will be handled by the web plugin class.
Also, this is the most suitable place to use the allowInterop method to create a function that can be called from JavaScript.

The registerWith method code:
flutter_plugin/lib/flutter_plugin_web.dart

static void registerWith(Registrar registrar) {
 final MethodChannel channel = MethodChannel(
   'flutter_plugin',
   const StandardMethodCodec(),
   registrar,
 );
 
 final pluginInstance = FlutterWebPluginExampleWeb();
 channel.setMethodCallHandler(pluginInstance.handleMethodCall);
 
 //Sets the call from JavaScript handler
 _jsOnEvent = allowInterop((dynamic event) {
   //Process JavaScript call here
 });
}

The full code of web plugin class is provided:
flutter_plugin/lib/flutter_plugin_web.dart

@JS()
library callable_function;
 
import 'dart:async';
 
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
 
@JS('jsOnEvent')
external set _jsOnEvent(void Function(dynamic event) f);
 
@JS()
external dynamic jsInvokeMethod(String method, String? params);
 
class FlutterPluginWeb {
 static void registerWith(Registrar registrar) {
   final MethodChannel channel = MethodChannel(
     'flutter_plugin',
     const StandardMethodCodec(),
     registrar,
   );
 
   final pluginInstance = FlutterPluginWeb();
   channel.setMethodCallHandler(pluginInstance.handleMethodCall);
 
   //Sets the call from JavaScript handler
   _jsOnEvent = allowInterop((dynamic event) {
     //Process JavaScript call here
   });
 }
 
 Future<dynamic> handleMethodCall(MethodCall call) async {
   switch (call.method) {
     case 'sendMethodMessage':
       return sendMethodMessage(call.method, call.arguments);
     default:
       throw PlatformException(
         code: 'Unimplemented',
         details:
                 'flutter_plugin for web doesn\'t implement \'${call.method}\'',
       );
   }
 }
 
 Future<dynamic> sendMethodMessage(
     String method, String? arguments) async {
   final dynamic response =
       await promiseToFuture(jsInvokeMethod(method, arguments));
   //...
 }
}

Conclusion

That’s all! Now you can add web support for existing plugins or create your own ones. Hope this article has been helpful to you.

Brainstorming the idea on how to build
a great web app?

Share your idea with us, and we’ll come up with the best development solution for your case.

Get in touch today