There are a couple of JS based QR-Code readers out there. As an example
so why bother implementing yet another solution? Pretty damn easy: none of them works on the mobile web-browsers. I spend more time trying to get them running on mobile than I have spent on my own implementation. Since we now know the why, it is time for the how.
Pub dependencies
flutter pub add universal_html
flutter pub add universal_ui
flutter pub add js
Fetch all dependencies with
flutter pub get
Now that we are all set with our flutter dependencies, the pubspec.yaml should be similar to
dependencies:
flutter:
sdk: flutter
universal_html: ^2.0.8
universal_ui: ^0.0.8
js: ^0.6.3
JS dependencies
For QR-Code detection we are going to use jsQR and a (shitty and prototype-like) custom JS for permission request + camera-feed streaming. Download both .JS files and add them to the folder /web/js/
Add the .JS files to the index.html. E.g.:
<!DOCTYPE html>
<html>
<head>
<base href="/">
...
<script src="js/jsQR.js" type="application/javascript"></script>
<script src="js/camera.js" type="application/javascript"></script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
Dart JS-Wrapper
For intercommunication between dart and JS, we are going to create a wrapper class. This class not only helps with simple JS calls, but is also capable of wrapping a JS promise into a dart future. Create a js_wrapper.dart and paste the following code:
@JS()
library camera;
import 'package:js/js.dart';
// ignore: avoid_web_libraries_in_flutter
import 'dart:js_util';
@JS('startCameraFeed')
external dynamic _startCameraFeed(Object videoElement, Object canvasElement, int interval);
Future<bool> startCameraFeed(Object videoElement, Object canvasElement, int interval) => promiseToFuture(_startCameraFeed(videoElement, canvasElement, interval));
@JS('stopCameraFeed')
external void stopCameraFeed();
@JS('getQRCode')
external String getQRCode();
QR-Reader Widget
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:universal_ui/universal_ui.dart';
import 'package:universal_html/html.dart' as html;
import 'dart:async';
import 'js_wrapper.dart';
typedef void QrValue(String value);
class QrReader extends StatefulWidget {
final _QrReaderState _qrReaderState;
final QrValue callback;
QrReader({this.callback})
: _qrReaderState = _QrReaderState(qrCodeCallback: callback);
Future<bool> startCamera() => _qrReaderState.startCamera();
void stopCamera() => _qrReaderState.stopCamera();
bool get isScanning => _qrReaderState.isScanning;
@override
State<StatefulWidget> createState() => _qrReaderState;
}
class _QrReaderState extends State<QrReader> {
final QrValue qrCodeCallback;
final String _videoElementName;
final String _videoElementId;
final String _canvasElementName;
final String _canvasElementNameId;
final html.VideoElement _videoElement;
final html.CanvasElement _canvasElement;
final int _refreshInterval = 25;
Timer _checkForQRCodeTimer;
_QrReaderState({this.qrCodeCallback})
: _videoElement = html.VideoElement(),
_videoElementName = "camera",
_videoElementId = "camera_id",
_canvasElementName = "cameraCanvas",
_canvasElementNameId = "cameraCanvas_id",
_canvasElement = html.CanvasElement() {
_videoElement.id = _videoElementId;
_videoElement.autoplay = true;
_videoElement.style.display = "none";
_canvasElement.id = _canvasElementNameId;
}
@override
Widget build(BuildContext context) {
// define our HTML element to render
ui.platformViewRegistry
.registerViewFactory(_videoElementName, (int viewId) => _videoElement);
ui.platformViewRegistry.registerViewFactory(
_canvasElementName, (int viewId) => _canvasElement);
return Container(
child: new Stack(children: <Widget>[
HtmlElementView(viewType: _videoElementName),
HtmlElementView(viewType: _canvasElementName),
]),
);
}
bool get isScanning => _checkForQRCodeTimer?.isActive ?? false;
Future<bool> startCamera() async {
// ensure that the camera isn't running
stopCamera();
// 1. start the camera input stream
bool couldStartCamera =
await startCameraFeed(_videoElement, _canvasElement, _refreshInterval);
if (couldStartCamera)
// 2. start the internal scan timer
_checkForQRCodeTimer = Timer.periodic(
Duration(milliseconds: _refreshInterval),
(Timer t) => _tryObtainQRCode());
return couldStartCamera;
}
void stopCamera() {
// 1. abort the camera input stream
stopCameraFeed();
// 2. abort the internal timer
_checkForQRCodeTimer?.cancel();
}
void _tryObtainQRCode() {
String code = getQRCode();
if (code.isNotEmpty) qrCodeCallback(code);
}
@override
void dispose() {
stopCamera();
super.dispose();
}
}