Barcode Scanner for Flutter Web App

Flutter Web App OnLine demo

Flutter is a powerful framework from Google for building cross-platform applications with a single codebase.

In this tutorial, we'll go over how to create a barcode scanning app specifically for the Web. To do this, we will use our DataSymbol Web SDK.

Why only Web? Because barcode scanning requires a platform-dependent library. We have libraries for all platforms, but in this article, we’ll only be creating a Web application.


Getting Started

Download the Barcode Scanner Web SDK. Out of all the files, we only need two: "datasymbol-sdk.wasm" and "datasymbol-sdk-hlp.min.js".

Create a sample Flutter project. Inside the Project Explorer, you’ll see a structure similar to:

Create Sample Flutter project in any development environment

Copy the "datasymbol-sdk.wasm" and "datasymbol-sdk-hlp.min.js" files into the "web" folder of your project.

In the web folder, create a file named "ds_create_scanner.js". We will provide its contents shortly.

Open the "/web/index.html" file. Inside the <head> tag, add the following lines:
  <script defer type="text/javascript" src="datasymbol-sdk-hlp.min.js"></script> 
  <script type="text/javascript" src="ds_create_scanner.js"></script>


Now everything is ready. Let’s continue.


How It Works

If you compile the sample Flutter project for Web:
# flutter build web
in the browser inspector, you’ll see:

Flutter canvas renders all widgets

A single canvas element takes up the entire browser window. The Flutter framework generates code that renders all its widgets inside this canvas. So how can we easily embed our Barcode Scanner here?

Here’s one option that seems the simplest to us: in short, we embed HTML code that will render over the Flutter "canvas".

First, let’s populate the "ds_create_scanner.js" file. The rest of the document includes JavaScript and Dart code with embedded comments explaining functions like CreateScanner, embedScanner, and the logic behind rendering and positioning the scanner overlay.

Complete Code of "ds_create_scanner.js" file

var _scannerVariables = {
    elemScannerDiv: null,
    DEF_BARCODE_TYPE: ['Code128', 'Code39', 'EAN13', 'UPCA', 'QRCode'],
    camDevices: null,
    bDSMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
    bIsIOS: ['iPad', 'iPhone', 'iPod'].includes(navigator.platform) ||
				// iPad on iOS detection
				(navigator.userAgent.includes("Mac") && "ontouchend" in document),
    camName: ['0, facing back', 'facing back:0', 'back camera'],
};


//returns camera idx
function SelectBestCamera( camArr, cams )
{
	var _result = -1;
	camArr.forEach(function (c) { c.label = c.label.toLowerCase(); });
	cams.forEach(function (c) { c = c.toLowerCase(); });
	for( var i=0; i < camArr.length; ++i )
	{
		for( var j=0; j < cams.length; ++j )
		{
			if( camArr[i].label.includes(cams[j]) )
				return i;
		}
	}
	return _result;
}

function onDSBarcode (barcodeResult)
{
	for (var i = 0; i < barcodeResult.length; ++i) {
	    var sBarcode = DSScanner.bin2String(barcodeResult[i]);
        var dr = barcodeResult[i];
        dr.strdata = sBarcode;
        if (typeof _scannerVariables['onBarcode'] === "function")
            _scannerVariables['onBarcode'](dr);
    }
};

function onDSError(err)
{
    if (typeof _scannerVariables['onError'] === "function")
	    _scannerVariables['onError'](err);
}

function CreateScanner()
{
    var scannerSettings = {
        scanner: {
            key: '',
            frameTimeout:	100,
            barcodeTimeout:	1000,
            beep: true,
        },
        viewport: {
            id: 'datasymbol-barcode-viewport',
            width: _scannerVariables.bDSMobile ? null : 640,	//null means 100% width
        },
        camera: {
            resx: 640,
            resy: 480,
        },
        barcode: {
            barcodeTypes: _scannerVariables.DEF_BARCODE_TYPE,
            bQRCodeFindMicro: false,
            frameTime: 1000,
        },
    };

    if( _scannerVariables.camDevices && _scannerVariables.camDevices.length > 0 )
    {
            if( _scannerVariables.bDSMobile || _scannerVariables.bIsIOS )
            {
                var camIdx = SelectBestCamera( _scannerVariables.camDevices, _scannerVariables.camName );
                if( camIdx >= 0 )
                    scannerSettings.camera.id = _scannerVariables.camDevices[camIdx].id;
                else
                    scannerSettings.camera.facingMode = 'environment';
            }
            //non mobile, select first camera
            else
            {
                scannerSettings.camera.id = _scannerVariables.camDevices[0].id
                scannerSettings.camera.label = _scannerVariables.camDevices[0].label;
            }
    }
        
    DSScanner.addEventListener('onBarcode', onDSBarcode);

    DSScanner.addEventListener('onScannerReady', function () {
        DSScanner.StartScanner();
    });

    DSScanner.Create(scannerSettings);
}

function embedScanner( x, y, width, height )
{
    _scannerVariables.elemScannerDiv = document.createElement('div');
    _scannerVariables.elemScannerDiv.id = 'div-datasymbol-barcode-viewport';
    _scannerVariables.elemScannerDiv.style.cssText = 'position:absolute;left:'+x+'px;top:'+y+'px;width:'+width+'px;height:'+height+'px;z-index:100;';

    _scannerVariables.elemScannerDiv.innerHTML = `
        <div id="datasymbol-barcode-viewport" style='display:block;width:100%;height:480px;font: bold 2em Tahoma;'></div>
    `;

    document.body.appendChild(_scannerVariables.elemScannerDiv);

    DSScanner.addEventListener('onError', onDSError);

	DSScanner.getVideoDevices(function (devices) {
		if(devices.length > 0)
		{
			_scannerVariables.camDevices = devices.slice();
			CreateScanner();
		}
		else
		{
			onDSError( {name: 'NotFoundError', message: 'Camera Not Connected'} );
		}
	}, true);

//    CreateScanner();
}

function updateScannerPos(x, y, width, height)
{
    if( _scannerVariables.elemScannerDiv )
    {
        _scannerVariables.elemScannerDiv.style.left = x + 'px';
        _scannerVariables.elemScannerDiv.style.top = y + 'px';
        _scannerVariables.elemScannerDiv.style.width = width + 'px';
        _scannerVariables.elemScannerDiv.style.height = height + 'px';
    }
}

function addScannerCallback(funcName, dsFuncCallback)
{
    if(funcName && funcName.length !=0 && dsFuncCallback != null && (typeof dsFuncCallback === "function") )
        _scannerVariables[funcName] = dsFuncCallback;
}

The method "embedScanner" will be called from "main.dart" to create and start the Barcode Scanner. This JavaScript code also includes various methods for setting up and operating the scanner.

To ensure the barcode scanner appears in the correct spot in the browser window, we use the "SizedBox" widget (see the "main.dart") file). Every time the "SizedBox" changes its coordinates (e.g., when resizing the browser window), we update the position of our barcode scanner accordingly.



"main.dart"


Open the "/lib/main.dart" file of your Flutter project and copy the code provided in the document. It handles:
  • The creation of the Flutter UI.
  • Interfacing with JavaScript functions (embedScanner, updateScannerPos, addScannerCallback).
  • Handling events from the scanner.
  • Dynamically positioning and resizing the Barcode Scanner overlay using GlobalKey and Flutter’s rendering pipeline.
  • Descriptions of methods and types for interaction between Dart and JavaScript code.


Complete Code of "main.dart" file

import 'dart:math';

import 'package:flutter/material.dart';
import 'dart:js_interop';

extension type DSBarcodeError(JSObject _) implements JSObject {
  external String name;
  external String message;
}

extension type DSBarcodeResultCoord(JSObject _) implements JSObject {
  external int x;
  external int y;
}

extension type DSBarcodeResult(JSObject _) implements JSObject {
  external int bt;
  external String type;
  external JSInt8Array data;
  external String strdata;
  external JSArray<DSBarcodeResultCoord> points;
  external bool barcodeAtPoint;
  external bool previouslyDecoded;
}

@JS()
external void embedScanner(int x, int y, int width, int height);

@JS()
external void updateScannerPos(int x, int y, int width, int height);

@JS()
external void addScannerCallback(JSString funcName, JSFunction dsFuncCallback);


void main() {
  runApp( MyApp() );
}


class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  final GlobalKey _key = GlobalKey();

  bool _firstStart = true;
  String _strBarcode = '';

  //DataSymbol.com Barcode Scanner callback function. Fired when a barcode event occurs.
  void onDSBarcode (DSBarcodeResult dr) { 
    _strBarcode = dr.strdata;
    setState( (){} );
  }

  //DataSymbol.com Barcode Scanner callback function. Fired when a scanner error event occurs.
  void onDSError (DSBarcodeError err) { 
    _strBarcode = '${err.name}, ${err.message}';
    setState( (){} );
  }

  Rectangle<int> getBarcodeViewBox() {
    final RenderBox renderBox = _key.currentContext!.findRenderObject() as RenderBox;
    final position = renderBox.localToGlobal(Offset.zero);
    return Rectangle( position.dx.toInt(), position.dy.toInt(), renderBox.size.width.toInt(), renderBox.size.height.toInt() );
  }

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
      WidgetsBinding.instance.removeObserver(this);
      super.dispose();
  }

  @override
  void didChangeMetrics() {
    WidgetsBinding.instance.addPostFrameCallback( (timestamp){
      Rectangle rc = getBarcodeViewBox();
      updateScannerPos( rc.left.toInt(), rc.top.toInt(), rc.width.toInt(), rc.height.toInt() );
    });
  }

  void startUpFunction() async {
    if( _firstStart )
    {
      //Embed and Start DataSymbol.com Barcode Scanner

      addScannerCallback('onError'.toJS, onDSError.toJS);
      addScannerCallback('onBarcode'.toJS, onDSBarcode.toJS);

      Rectangle rc = getBarcodeViewBox();
      embedScanner(rc.left.toInt(), rc.top.toInt(), rc.width.toInt(), rc.height.toInt());
    }

    _firstStart = false;
  }

  @override
  Widget build(BuildContext context) {

     WidgetsBinding.instance.addPostFrameCallback((_) => startUpFunction());

     return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Barcode:'),
            Text(
              _strBarcode,
              style: Theme.of(context).textTheme.headlineMedium,
            ),

            SizedBox(// Container(
              key: _key,
              width: 640,
              height: 480,
              //color: Colors.grey[300],
              child: Text(
                'Barcode Scanner Loading ...',
              ),
            ),

          ],
        ),
      ),
    );
  }
}



A live demo of DataSymbol.com Barcode Scanner in Flutter App is available online:
OnLine demo

A complete Flutter project with the Barcode Scanner can be downloaded here:
Download