DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

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

What You Should Know

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}\'',
        );
    }
  }
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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 = () => {};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Initializing Barcode Reader and Barcode Scanner objects are as follows:

/// 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)});
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

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)))});
Enter fullscreen mode Exit fullscreen mode

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@8.2.3/dist/dbr.js" data-productKeys="PRODUCT-KEYS"></script> to web/index.html:

<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@8.2.3/dist/dbr.js" data-productKeys="PRODUCT-KEYS"></script>
<script src="main.dart.js" type="application/javascript"></script>
Enter fullscreen mode Exit fullscreen mode

Create two material buttons. One for decoding barcodes from static images, and the other for scanning barcodes 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)});
                      }),
                ]),
          ),
        ])),
  );
Enter fullscreen mode Exit fullscreen mode

Finally, we run the web app in Chrome:

flutter run -d chrome
Enter fullscreen mode Exit fullscreen mode

Flutter barcode reader

Source Code

https://github.com/yushulx/flutter_barcode_sdk

Discussion (0)