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.
This article is Part 3 in a 5-Part Series.
- Part 1 - How to Implement Flutter Barcode Scanner for Android
- Part 2 - Flutter Barcode Plugin - Writing C++ Code for Windows Desktop
- Part 3 - Flutter Barcode Plugin for Web: Interop between Dart and JavaScript
- Part 4 - Flutter Barcode Plugin for Linux: from Dart to C++
- Part 5 - Flutter Barcode SDK for iOS and macOS
Pub.dev Package
https://pub.dev/packages/flutter_barcode_sdk
SDK Activation
References
- https://dart.dev/web/js-interop
- https://github.com/grandnexus/firebase-dart
- 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:
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