DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build a Web Barcode, QR code and PDF417 Scanner in Flutter

Flutter enables developers to create applications for desktop, mobile, and web from a single codebase. However, not all plugins are universally compatible with every platform. In this article, you will learn how to choose appropriate plugins to build a web-based barcode, QR code, and PDF417 scanner in Flutter. Although the example can also run on desktop and mobile, the camera scanning feature is only available on the web.

Try Online Demo

https://yushulx.me/flutter-web-barcode-qrcode-pdf417-scanner

flutter web barcode qr scanner

Create a Flutter Project and Install Dependencies for Web

Run the following command to create a Flutter project and install dependencies.

flutter create barcode_scanner
cd barcode_scanner
flutter pub add camera image_picker flutter_barcode_sdk file_selector url_launcher
Enter fullscreen mode Exit fullscreen mode
  • camera: A plugin for accessing the camera on Android, iOS, and web platforms.
  • image_picker and file_selector: Plugins for selecting images from the local file system on the web. Both can be used, but neither covers all platforms. If you want to support all platforms, you need to use both.
  • flutter_barcode_sdk: A plugin for integrating the Dynamsoft Barcode Reader SDK into your Flutter project. It supports Windows, Linux, macOS, Android, iOS, and web. To make the plugin work on the web, you need to include the JavaScript files in the index.html file.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.0/dist/dbr.js"></script>
    
  • url_launcher: A plugin for opening URLs in the default browser.

Implement the Web Barcode, QR code and PDF417 Scanner Step by Step

The app consists of the following screens:

  • Home screen: The home screen includes a floating action button for navigating to the barcode type setting screen, an elevated button for navigating to the file reader screen, and an elevated button for navigating to the camera scanner screen.
  • Barcode type setting screen: The barcode type setting screen allows you to select the barcode types to scan.
  • File reader screen: The file reader screen allows you to select an image file from the local file system and scan the barcode in the image.
  • Camera scanner screen: The camera scanner screen allows you to scan the barcode in the camera preview.

Home Screen

home screen

We initialize the Dynamsoft Barcode Reader SDK with a trial license in the main.dart file.

class _MyHomePageState extends State<MyHomePage> {
  late FlutterBarcodeSdk _barcodeReader;
  bool _isSDKLoaded = false;

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

    initBarcodeSDK();
  }

  Future<void> initBarcodeSDK() async {
    _barcodeReader = FlutterBarcodeSdk();
    await _barcodeReader.setLicense(
        'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==');
    await _barcodeReader.init();
    await _barcodeReader.setBarcodeFormats(BarcodeFormat.ALL);
    setState(() {
      _isSDKLoaded = true;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The boolean variable _isSDKLoaded is used to indicate whether the Dynamsoft Barcode Reader SDK is loaded. When loading the SDK first time, it takes longer time because the SDK needs to download the JavaScript files and wasm files. If the variable is false, we show a progress indicator.

Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        const CircularProgressIndicator(),
        const SizedBox(height: 16),
        Text.rich(
        TextSpan(
            text: 'Loading ',            
            style: const TextStyle(fontSize: 16),
            children: <TextSpan>[
            TextSpan(
                text: 'Dynamsoft Barcode Reader',
                style: const TextStyle(
                decoration: TextDecoration.underline,
                color: Colors.blue,
                fontSize: 16,
                ),
                recognizer: TapGestureRecognizer()
                ..onTap = () {
                    launchUrlString(
                        'https://www.dynamsoft.com/barcode-reader/sdk-javascript/');
                },
            ),
            const TextSpan(
                text:
                    ' js and wasm files...The first time may take a few seconds.'),
            ],
        ),
        ),
    ],
    ),
Enter fullscreen mode Exit fullscreen mode

The launchUrlString() method provided by the url_launcher plugin is used to open the URL in the default browser.

As the SDK is loaded, you will see a hint message pop up if you are using a one-day trial.

Dynamsoft JavaScript SDK hint message

The two elevated buttons are used for screen navigation.

Column(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
        ElevatedButton(
        onPressed: () {
            if (_isSDKLoaded == false) {
            _showDialog('Error', 'Barcode SDK is not loaded.');
            return;
            }

            Navigator.push(
            context,
            MaterialPageRoute(
                builder: (context) => ReaderScreen(
                        barcodeReader: _barcodeReader,
                    )),
            );
        },
        child: const Text('Barcode Reader'),
        ),
        ElevatedButton(
        onPressed: () {
            if (_isSDKLoaded == false) {
            _showDialog('Error', 'Barcode SDK is not loaded.');
            return;
            }

            if (!kIsWeb) {
            _showDialog('Error',
                'Barcode Scanner is only supported on Web.');
            return;
            }

            Navigator.push(
            context,
            MaterialPageRoute(
                builder: (context) => ScannerScreen(
                        barcodeReader: _barcodeReader,
                    )),
            );
        },
        child: const Text('Barcode Scanner'),
        ),
    ],
    )
Enter fullscreen mode Exit fullscreen mode

When the SDK is not loaded and the button is clicked, an alert dialog is shown.

_showDialog(String title, String message) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(title),
          content: Text(message),
          actions: <Widget>[
            TextButton(
              child: const Text('OK'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
Enter fullscreen mode Exit fullscreen mode

The floating action button not only navigates to the barcode type setting screen, but also receives the selected barcode types from it.

FloatingActionButton(
        onPressed: () async {
          if (_isSDKLoaded == false) {
            _showDialog('Error', 'Barcode SDK is not loaded.');
            return;
          }
          var result = await Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => const SettingsScreen()),
          );
          int format = result['format'];
          await _barcodeReader.setBarcodeFormats(format);
        },
        tooltip: 'Settings',
        child: const Icon(Icons.settings),
      ),
Enter fullscreen mode Exit fullscreen mode

Barcode Type Setting Screen

The barcode type setting screen allows you to select the barcode types to be recognized.

Flutter barcode type checkbox

We create three check boxes for each barcode type. The onChanged callback is used to update the format variable.

ListView(
    children: <Widget>[
    CheckboxListTile(
        title: const Text('1D Barcode'),
        value: _is1dChecked,
        onChanged: (bool? value) {
        setState(() {
            _is1dChecked = value!;
        });
        },
    ),
    CheckboxListTile(
        title: const Text('QR Code'),
        value: _isQrChecked,
        onChanged: (bool? value) {
        setState(() {
            _isQrChecked = value!;
        });
        },
    ),
    CheckboxListTile(
        title: const Text('PDF417'),
        value: _isPdf417Checked,
        onChanged: (bool? value) {
        setState(() {
            _isPdf417Checked = value!;
        });
        },
    ),
    ],
)
Enter fullscreen mode Exit fullscreen mode

The WillPopScope() widget is used to intercept the back button event. When the back button is clicked, we return the selected barcode types to the previous screen.

return WillPopScope(
      // override the pop action
      onWillPop: () async {
        int format = 0;
        if (_is1dChecked) {
          format |= BarcodeFormat.ONED;
        }
        if (_isQrChecked) {
          format |= BarcodeFormat.QR_CODE;
        }
        if (_isPdf417Checked) {
          format |= BarcodeFormat.PDF417;
        }
        Navigator.pop(context, {'format': format});
        return true;
      },)
Enter fullscreen mode Exit fullscreen mode

File Reader screen

The file reader screen allows you to select a local image file and recognize the barcode in it. We need to create an image widget to display the selected image file and an overlay widget to display the recognized barcode results. The size of the overlay should be the same as the image widget.

Flutter image and overlay

The effect can be achieved by using the FittedBox widget and the Stack widget:

Widget createOverlay(List<BarcodeResult> results) {
  return CustomPaint(
    painter: OverlayPainter(results),
  );
}

Widget getImage() {
    if (_file != null) {
      Image image = kIsWeb
          ? Image.network(
              _file!,
            )
          : Image.file(
              File(_file!),
            );
      return image;
    }
    return Image.asset(
      'images/default.png',
    );
  }

FittedBox(
    fit: BoxFit.contain,
    child: Stack(
    children: [
        getImage(),
        Positioned(
        top: 0.0,
        right: 0.0,
        bottom: 0.0,
        left: 0.0,
        child: _results == null || _results!.isEmpty
            ? Container(
                color: Colors.black.withOpacity(0.1),
                child: const Center(
                    child: Text(
                    'No barcode detected',
                    style: TextStyle(
                        color: Colors.white,
                        fontSize: 20.0,
                        fontWeight: FontWeight.bold,
                    ),
                    ),
                ))
            : createOverlay(_results!),
        ),
    ],
    ),
    )

Enter fullscreen mode Exit fullscreen mode

The overlay widget is extended from the CustomPainter class. We override the paint() method to draw the recognized barcode results on the canvas.

class OverlayPainter extends CustomPainter {
  final List<BarcodeResult> results;

  const OverlayPainter(this.results);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke;

    for (var result in results) {
      double minX = result.x1.toDouble();
      double minY = result.y1.toDouble();
      if (result.x2 < minX) minX = result.x2.toDouble();
      if (result.x3 < minX) minX = result.x3.toDouble();
      if (result.x4 < minX) minX = result.x4.toDouble();
      if (result.y2 < minY) minY = result.y2.toDouble();
      if (result.y3 < minY) minY = result.y3.toDouble();
      if (result.y4 < minY) minY = result.y4.toDouble();

      canvas.drawLine(Offset(result.x1.toDouble(), result.y1.toDouble()),
          Offset(result.x2.toDouble(), result.y2.toDouble()), paint);
      canvas.drawLine(Offset(result.x2.toDouble(), result.y2.toDouble()),
          Offset(result.x3.toDouble(), result.y3.toDouble()), paint);
      canvas.drawLine(Offset(result.x3.toDouble(), result.y3.toDouble()),
          Offset(result.x4.toDouble(), result.y4.toDouble()), paint);
      canvas.drawLine(Offset(result.x4.toDouble(), result.y4.toDouble()),
          Offset(result.x1.toDouble(), result.y1.toDouble()), paint);

      TextPainter textPainter = TextPainter(
        text: TextSpan(
          text: result.text,
          style: const TextStyle(
            color: Colors.blue,
            fontSize: 24.0,
          ),
        ),
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
      );
      textPainter.layout(minWidth: 0, maxWidth: size.width);
      textPainter.paint(canvas, Offset(minX, minY));
    }
  }

  @override
  bool shouldRepaint(OverlayPainter oldDelegate) =>
      results != oldDelegate.results;
}
Enter fullscreen mode Exit fullscreen mode

Flutter barcode image reader and overlay

Camera Scanner screen

In the camera scanner screen, the camera plugin is used to control cameras.

class _ScannerScreenState extends State<ScannerScreen> {
  late FlutterBarcodeSdk _barcodeReader;
  late List<CameraDescription> _cameras;
  CameraController? _controller;
  bool _isCameraReady = false;
  String _selectedItem = '';
  final List<String> _cameraNames = [''];
  bool _loading = true;
  bool _isTakingPicture = false;
  List<BarcodeResult>? _results;
  Size? _previewSize;

  @override
  void initState() {
    super.initState();
    _barcodeReader = widget.barcodeReader;
    initCamera();
  }

  Future<void> toggleCamera(int index) async {
    _isCameraReady = false;
    if (_controller != null) _controller!.dispose();

    _controller = CameraController(_cameras[index], ResolutionPreset.max);
    _controller!.initialize().then((_) {
      if (!mounted) {
        return;
      }

      _isCameraReady = true;
      _previewSize = _controller!.value.previewSize;
      setState(() {});

      decodeFrames();
    }).catchError((Object e) {
      if (e is CameraException) {
        switch (e.code) {
          case 'CameraAccessDenied':
            break;
          default:
            break;
        }
      }
    });

    setState(() {
      _selectedItem = _cameras[index].name;
    });
  }

  Future<void> initCamera() async {
    try {
      WidgetsFlutterBinding.ensureInitialized();
      _cameras = await availableCameras();
      if (_cameras.isEmpty) return;

      _cameraNames.clear();
      for (CameraDescription description in _cameras) {
        _cameraNames.add(description.name);
      }
      _selectedItem = _cameraNames[0];

      toggleCamera(0);
    } on CameraException catch (e) {
      print(e);
    }

    setState(() {
      _loading = false;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The CameraPreview widget plays the camera stream. It needs to be wrapped in a SizedBox widget to set the size of the camera preview.

SizedBox(
    width: _previewSize == null
        ? 640
        : _previewSize!.width,
    height: _previewSize == null
        ? 480
        : _previewSize!.height,
    child: CameraPreview(
    _controller!,
    ))
Enter fullscreen mode Exit fullscreen mode

A dropdown list is used to select a camera. The onChanged callback is used to switch the camera.

DropdownButton<String>(
    value: _selectedItem,
    items: _cameraNames
        .map<DropdownMenuItem<String>>((String value) {
        return DropdownMenuItem<String>(
        value: value,
        child: Text(value),
        );
    }).toList(),
    onChanged: (String? newValue) {
        if (newValue == null || newValue == '') return;
        int index = _cameraNames.indexOf(newValue);
        toggleCamera(index);
    },
    )
Enter fullscreen mode Exit fullscreen mode

The code of the overlay part is the same as the image reader screen.

A critical part of the camera scanner screen is how to get the camera frames and decode them. The camera plugin provides a startImageStream method to get the streaming images, but it is only available on Android and iOS. To get the camera frames on the web, we can create a timer to take a picture every 20 milliseconds.

Future<void> decodeFrames() async {
    if (_controller == null || !_isCameraReady) return;

    Future.delayed(const Duration(milliseconds: 20), () async {
      if (_controller == null || !_isCameraReady) return;

      if (!_isTakingPicture) {
        _isTakingPicture = true;
        XFile file = await _controller!.takePicture();
        _results = await _barcodeReader.decodeFile(file.path);
        setState(() {});
        _isTakingPicture = false;
      }

      decodeFrames();
    });
  }

Enter fullscreen mode Exit fullscreen mode

Flutter barcode camera scanner and overlay

So far, the web barcode, QR code, and PDF417 scanner is completed. The next step is to deploy the app to GitHub Pages.

Deploy a Flutter Web App to GitHub Pages

To build and deploy a Flutter web app to GitHub Pages, follow these steps:

  1. Push the code to your GitHub repository.
  2. Create a workflow file .github/workflows/main.yml by clicking Actions -> New workflow.

    name: Build and Deploy Flutter Web
    
    on:
    push:
        branches:
        - main
    jobs:
    build:
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v2
        - name: Set up Flutter
        uses: subosito/flutter-action@v1
        with:
            channel: 'stable'
        - name: Get dependencies
        run: flutter pub get
        - uses: bluefireteam/flutter-gh-pages@v7
        with:
            baseHref: /flutter-web-barcode-qrcode-pdf417-scanner/
    

    The baseHref is the path to the web app. You can change it to your own path.

  3. When you push the code to the main branch, it will trigger the workflow. The workflow will build the Flutter web app and deploy it to GitHub Pages.

Note: You may encounter an error message "Permission denied to github-actions[bot]" when building the project.

GitHub action bot permission denied

To fix this, follow these steps:

  1. Go to your repository's Settings.
  2. Click on the "Actions" tab.
  3. Click "General" and then select "Workflow permissions."
  4. Grant the permission to the bot.

GitHub action with read and write permission

Once the build is successful, you can deploy the web app to GitHub Pages by following these steps:

  1. Go to your repository's Settings.
  2. Click on the "Pages" tab.
  3. Select the branch to deploy and the folder where your web app is built.
  4. Click "Save" to deploy the web app.

Flutter web app to GitHub pages

Source Code

https://github.com/yushulx/flutter-web-barcode-qrcode-pdf417-scanner

Oldest comments (0)