Flutter PWA QR-Code Reader (on mobile!)

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();
  }
}
5 1 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments