How to Build a Web Barcode, QR code and PDF417 Scanner in Flutter
Tags: flutter, dart, webdev, qrcode
Published: 2023-02-17 00:15:40

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](https://yushulx.me/flutter-web-barcode-qrcode-pdf417-scanner)


![flutter web barcode qr scanner](https://www.dynamsoft.com/codepool/img/2023/02/barcode-qrcode-pdf417-scanner.gif)

## Create a Flutter Project and Install Dependencies for Web
Run the following command to create a Flutter project and install dependencies.

```bash
flutter create barcode_scanner
cd barcode_scanner
flutter pub add camera image_picker flutter_barcode_sdk file_selector url_launcher
```

- [camera](https://pub.dev/packages/camera): A plugin for accessing the camera on Android, iOS, and web platforms.
- [image_picker](https://pub.dev/packages/image_picker) and [file_selector](https://pub.dev/packages/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](https://pub.dev/packages/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. 
    ```html
    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.0/dist/dbr.js"></script>
    ```
- [url_launcher](https://pub.dev/packages/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](https://www.dynamsoft.com/codepool/img/2023/02/barcode-qr-scanner-homepage.png)

We initialize the Dynamsoft Barcode Reader SDK  with a [trial license](https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr) in the `main.dart` file.

```dart
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;
    });
  }
}
```

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.

```dart
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.'),
            ],
        ),
        ),
    ],
    ),
```

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](https://www.dynamsoft.com/codepool/img/2023/02/dynamsoft-javascript-barcode-trial-license.png)

The two elevated buttons are used for screen navigation.

```dart
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'),
        ),
    ],
    )
```

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

```dart
_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();
              },
            ),
          ],
        );
      },
    );
  }
```

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

```dart
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),
      ),
```

### Barcode Type Setting Screen
The barcode type setting screen allows you to select the barcode types to be recognized. 


![Flutter barcode type checkbox](https://www.dynamsoft.com/codepool/img/2023/02/barcode-type-setting-screen.png)


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

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

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.

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

### 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](https://www.dynamsoft.com/codepool/img/2023/02/barcode-reader-screen.png)

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

```dart
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!),
        ),
    ],
    ),
    )

```

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

```dart
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;
}
```
![Flutter barcode image reader and overlay](https://www.dynamsoft.com/codepool/img/2023/02/flutter-web-barcode-qr-pdf417-reader.png)

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

```dart
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;
    });
  }
}
```

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

```dart
SizedBox(
    width: _previewSize == null
        ? 640
        : _previewSize!.width,
    height: _previewSize == null
        ? 480
        : _previewSize!.height,
    child: CameraPreview(
    _controller!,
    ))
```

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

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

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](https://pub.dev/documentation/camera/latest/camera/CameraController/startImageStream.html) 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.

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

```

![Flutter barcode camera scanner and overlay](https://www.dynamsoft.com/codepool/img/2023/02/flutter-web-barcode-qr-pdf417-scanner.png)

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`.

    ```yaml
    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](https://www.dynamsoft.com/codepool/img/2023/02/github-action-bot-permission-denied.png)

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](https://www.dynamsoft.com/codepool/img/2023/02/github-action-enable-write-permission.png)

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](https://www.dynamsoft.com/codepool/img/2023/02/github-page-flutter-web.png)




## Source Code
[https://github.com/yushulx/flutter-web-barcode-qrcode-pdf417-scanner](https://github.com/yushulx/flutter-web-barcode-qrcode-pdf417-scanner)