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

Xiao Ling
9 min readFeb 14, 2023

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

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
  • 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

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

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

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.

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

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

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

Barcode Type Setting Screen

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

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

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

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.

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

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;
}

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

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!,
))

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

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

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.

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.

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.

Source Code

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

Originally published at https://www.dynamsoft.com on February 14, 2023.

--

--

Xiao Ling

Manager of Dynamsoft Open Source Projects | Tech Lover