Flutter Barcode Plugin for Web: Interop between Dart and JavaScript
Published: 2021-05-13 06:01:19

This week, I made a new attempt, integrating [Dynamsoft JavaScript Barcode SDK](https://www.dynamsoft.com/barcode-reader/sdk-javascript/) into the ongoing [Flutter barcode plugin](https://pub.dev/packages/flutter_barcode_sdk/) 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](https://www.dynamsoft.com/codepool/img/2021/flutter/flutter-web-barcode-scanner.png)

## What You Should Know
- [![](https://img.shields.io/badge/Download-Offline%20SDK-orange)](https://www.dynamsoft.com/barcode-reader/downloads)
- [![](https://img.shields.io/badge/Get-30--day%20FREE%20Trial%20License-blue)](https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr)

## References
- [https://dart.dev/web/js-interop](https://dart.dev/web/js-interop)
- [https://github.com/grandnexus/firebase-dart](https://github.com/grandnexus/firebase-dart)
- [https://pub.dev/packages/js](https://pub.dev/packages/js)


## 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:

```yaml
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`:

```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:

```dart
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`:

```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:

```dart
Function globalCallback = () => {};
```

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

```dart
@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);
}
```

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

```dart
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:

```dart
/// 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:

```dart
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:

```dart
_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](https://pub.dev/packages/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`:

```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>
```

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

```dart
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](https://www.dynamsoft.com/codepool/img/2021/flutter/flutter-web-barcode-sdk.png)


## Source Code

[https://github.com/yushulx/flutter_barcode_sdk](https://github.com/yushulx/flutter_barcode_sdk)