Flutter Barcode Plugin for Web: Interop between Dart and JavaScript

This week, I made a new attempt, integrating Dynamsoft JavaScript Barcode SDK into the ongoing Flutter barcode plugin project. It is exciting to be capable of building web barcode reader apps with the same decodeFile() method used for mobile and desktop development.

flutter web barcode scanner

Pub.dev Package

https://pub.dev/packages/flutter_barcode_sdk

SDK Activation

References

Flutter Barcode SDK Plugin for Web Development

The above firebase_web is an excellent example of learning how to bridge Dart and JavaScript APIs. It inspired me a lot for Flutter web plugin implementation.

Initialize the Flutter plugin for web

Let’s add a web platform template to the existing plugin project:

flutter create --template=plugin --platforms=web .

The command only generates a flutter_barcode_sdk_web.dart file under the lib folder. Not like templates of other platforms, which generate independent folders containing some platform-specific language files, the web plugin command does not create any JavaScript file for us.

In addition, we need to add the web plugin description to the pubspec.yaml file:

flutter:
  plugin:
    platforms:
      android:
        package: com.dynamsoft.flutter_barcode_sdk
        pluginClass: FlutterBarcodeSdkPlugin
      windows:
        pluginClass: FlutterBarcodeSdkPlugin
      web:
        pluginClass: FlutterBarcodeSdkWeb
        fileName: flutter_barcode_sdk_web.dart

Implement Dart APIs for JavaScript interop

Similar to Android and Windows platforms, we find handleMethodCall() as the starting point in flutter_barcode_sdk_web.dart:

Future<dynamic> handleMethodCall(MethodCall call) async {
    switch (call.method) {
      case 'getPlatformVersion':
        return getPlatformVersion();
      default:
        throw PlatformException(
          code: 'Unimplemented',
          details:
              'flutter_barcode_sdk for web doesn\'t implement \'${call.method}\'',
        );
    }
  }

Since Dynamsoft JavaScript Barcode SDK provides Barcode Reader class for static images and Barcode Scanner class for video stream, I create a decodeFile() method and a decodeVideo() method correspondingly:

BarcodeManager _barcodeManager = BarcodeManager();

/// Decode barcodes from an image file.
Future<List<Map<dynamic, dynamic>>> decodeFile(String file) async {
  return _barcodeManager.decodeFile(file);
}

/// Decode barcodes from real-time video stream.
Future<void> decodeVideo() async {
  _barcodeManager.decodeVideo();
}

The decodeVideo() method is new and only available for web. It launchs the built-in HTML5 camera viewer of the JavaScript barcode SDK. We can register a callback function to receive the barcode decoding results. The calling method is defined in flutter_barcode_sdk.dart:

Future<void> decodeVideo(Function callback) async {
  globalCallback = callback;
  await _channel.invokeMethod('decodeVideo');
}

It is a little bit tricky here. Because the invokeMethod cannot pass function reference, my workaround is to assign the callback function to a global variable. To avoid build conflict among different platforms, the global variable is defined in a single global.dart file:

Function globalCallback = () => {};

Now, we focus on the most important barcode_manager.dart file, which defines Dart classes and functions for calling JavaScript APIs.

@JS('Dynamsoft')
library dynamsoft;

import 'dart:convert';
import 'dart:js';
import 'package:flutter_barcode_sdk/barcode_result.dart';
import 'package:flutter_barcode_sdk/global.dart';
import 'package:js/js.dart';
import 'utils.dart';

/// BarcodeScanner class
@JS('DBR.BarcodeScanner')
class BarcodeScanner {
  external static PromiseJsImpl<BarcodeScanner> createInstance();
  external void show();
  external set onFrameRead(Function func);
}

/// BarcodeReader class
@JS('DBR.BarcodeReader')
class BarcodeReader {
  external static PromiseJsImpl<BarcodeReader> createInstance();
  external PromiseJsImpl<List<dynamic>> decode(dynamic file);
  external static set productKeys(String license);
}

The following code is from firebase_web. Add them to a utils.dart file to handle JavaScript Promise.

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

typedef Func1<A, R> = R Function(A a);

@JS('JSON.stringify')
external String stringify(Object obj);

@JS('console.log')
external void log(Object obj);

@JS('Promise')
class PromiseJsImpl<T> extends ThenableJsImpl<T> {
  external PromiseJsImpl(Function resolver);
  external static PromiseJsImpl<List> all(List<PromiseJsImpl> values);
  external static PromiseJsImpl reject(error);
  external static PromiseJsImpl resolve(value);
}

@anonymous
@JS()
abstract class ThenableJsImpl<T> {
  external ThenableJsImpl then([Func1 onResolve, Func1 onReject]);
}

Future<T> handleThenable<T>(ThenableJsImpl<T> thenable) =>
    promiseToFuture(thenable);

Initializing Barcode Reader and Barcode Scanner objects are as follows:

/// Set license key.
Future<void> setLicense(String license) async {
  BarcodeReader.productKeys = license;
}

/// Initialize Barcode Scanner.
void initBarcodeScanner(BarcodeScanner scanner) {
  _barcodeScanner = scanner;
  _barcodeScanner.onFrameRead = allowInterop((results) =>
      {globalCallback(callbackResults(_resultWrapper(results)))});
}

/// Initialize Barcode Reader.
void initBarcodeReader(BarcodeReader reader) {
  _barcodeReader = reader;
}

BarcodeManager() {
  handleThenable(BarcodeScanner.createInstance())
      .then((scanner) => {initBarcodeScanner(scanner)});

  handleThenable(BarcodeReader.createInstance())
      .then((reader) => {initBarcodeReader(reader)});
}

The implementation of the decodeFile() method is fairly simple:

Future<List<Map<dynamic, dynamic>>> decodeFile(String filename) async {
    List<dynamic> barcodeResults =
        await handleThenable(_barcodeReader.decode(filename));

    return _resultWrapper(barcodeResults);
  }

As for the decodeVideo() method, to make Dart function callable from JavaScript, we use allowInterop() to wrap the Dart callback function:

_barcodeScanner.onFrameRead = allowInterop((results) =>
        {globalCallback(callbackResults(_resultWrapper(results)))});

Build web barcode reader and barcode scanner

So far, the Flutter web barcode plugin is done. We can test it by writing a simple Flutter app with image_picker.

Create a new Flutter web project and add <script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.0.0/dist/dbr.js"></script> to web/index.html:

<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.0.0/dist/dbr.js"></script>
<script src="main.dart.js" type="application/javascript"></script>

Initialize barcode reader object:

Future<void> initBarcodeSDK() async {
    _barcodeReader = FlutterBarcodeSdk();
    await _barcodeReader.setLicense(
        'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==');
    await _barcodeReader.init();
    await _barcodeReader.setBarcodeFormats(BarcodeFormat.ALL);
}

Create two material buttons. One for decoding barcodes and QR code from static images, and the other for scanning barcode and QR code from real-time video stream.

final picker = ImagePicker();

@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
        appBar: AppBar(
          title: const Text('Dynamsoft Barcode Reader'),
        ),
        body: Column(children: [
          Container(
            height: 100,
            child: Row(children: <Widget>[
              Text(
                _platformVersion,
                style: TextStyle(fontSize: 14, color: Colors.black),
              )
            ]),
          ),
          Expanded(
            child: SingleChildScrollView(
              child: Column(
                children: [
                  _file == null
                      ? Image.asset('images/default.png')
                      : Image.network(_file),
                  Text(
                    _barcodeResults,
                    style: TextStyle(fontSize: 14, color: Colors.black),
                  ),
                ],
              ),
            ),
          ),
          Container(
            height: 100,
            child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  MaterialButton(
                      child: Text('Barcode Reader'),
                      textColor: Colors.white,
                      color: Colors.blue,
                      onPressed: () async {
                        final pickedFile =
                            await picker.getImage(source: ImageSource.camera);

                        setState(() {
                          if (pickedFile != null) {
                            _file = pickedFile.path;
                          } else {
                            print('No image selected.');
                          }

                          _barcodeResults = '';
                        });

                        if (_file != null) {
                          List<BarcodeResult> results =
                              await _barcodeReader.decodeFile(_file);
                          updateResults(results);
                        }
                      }),
                  MaterialButton(
                      child: Text('Barcode Scanner'),
                      textColor: Colors.white,
                      color: Colors.blue,
                      onPressed: () async {
                        _barcodeReader.decodeVideo(
                            (results) => {updateResults(results)});
                      }),
                ]),
          ),
        ])),
  );

Finally, we run the web app in Chrome:

flutter run -d chrome

Flutter barcode reader

Source Code

https://github.com/yushulx/flutter_barcode_sdk