文档扫描与图像处理

Flutter Web插件实现:打通JavaScript和Dart

之前分享了如何在Flutter插件中支持Android和Windows,这篇文章将增加Web插件的实现方法,以及创建一个简单的web一维码,二维码识别应用。

Flutter barcode scanner

参考资源

  • https://dart.dev/web/js-interop
  • https://github.com/grandnexus/firebase-dart
  • https://pub.dev/packages/js

开发Flutter Web插件

Web插件开发,主要问题是如何实现Dart和JavaScript的相互调用。官网提供的firebase_web示例值得学习和参考。

初始化web插件

在当前的插件工程中增加Web模板:

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

和Windows, Android不同,web模板没有生成一个叫web的目录,也没有生成任何的JavaScritp代码文件。我们只看到一个新的flutter_barcode_sdk_web.dart文件。

接下来把插件声明添加到pubspec.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

如何实现JavaScript和Dart交互

和其它平台一样,handleMethodCall()是入口:

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}\'',
        );
    }
  }

但不同的是,web并不需要在插件中引入依赖库编译,我们要做的只是定义接口。

我定义了两个接口:decodeFile()decodeVideo(),分别用于通过图像和通过视频来识别一维码和二维码。

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

在视频模式下,结果是通过回调返回的。然而,在上层的flutter_barcode_sdk.dart中,回调函数引用没有办法通过invokeMethod传递下来。我的解决方法是使用全局变量保存回调函数:

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

为了避免和其它平台冲突,这个变量单独定义在一个global.dart 文件中:

Function globalCallback = () => {};

现在打开barcode_manager.dart,根据dynamsoft-javascript-barcode定义JavaScript的调用接口:

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

为了实现JavaScript的Promise,我们需要在另外一个utils.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);

接下来实现对象初始化:

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

Dart的函数需要通过allowInterop()封装才能够被JavaScript调用。

实现decodeFile()

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

    return _resultWrapper(barcodeResults);
  }

实现decodeVideo()回调:

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

创建Web一维码,二维码识别程序

创建一个新的Flutter工程,并在web/index.html中添加<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@8.2.3/dist/dbr.js" data-productKeys="PRODUCT-KEYS"></script>

pubspec.yaml中添加image_pickerflutter_barcode_sdk:

dependencies:
  flutter_barcode_sdk:
  image_picker:

在UI中添加两个按钮,一个用于加载图片,一个用于开启摄像头视频流:

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)});
                      }),
                ]),
          ),
        ])),
  );

最后运行程序:

flutter run -d chrome

Flutter barcode reader

Flutter插件下载

https://pub.dev/packages/flutter_barcode_sdk

源码

https://github.com/yushulx/flutter_barcode_sdk