Skip to main content

universal_barcode_scanner 2.1.2 copy "universal_barcode_scanner: ^2.1.2" to clipboard
universal_barcode_scanner: ^2.1.2 copied to clipboard

Barcode and QR code scanner for Flutter, on Android, iOS, Linux, macOS, web and Windows, from one widget and one callback.

Universal Barcode Scanner #

Barcode and QR code scanning for Flutter, on Android, iOS, Linux, macOS, web and Windows, from one entry point.

Scanning a barcode on Android    The camera opening inside a tile of the example app, on Android

Scanning a QR code in a browser

A barcode on Android, the camera inside the app's own layout, and a QR code in a browser.

Live demo Pub Version Build Maintainer Licence Platforms

Platforms #

Platform How it scans Embedded view
Android Native, CameraX and ML Kit Native view
iOS Native, AVFoundation Native view
macOS Native, AVFoundation and Vision Native view
Web zxing-cpp in WebAssembly, in a worker, bundled, no CDN call The same page in an iframe
Windows zxing-cpp in WebAssembly, in a WebView2 The same page in a WebView2
Linux zxing-cpp in WebAssembly, in a WebKitGTK view The same page in a WebKitGTK view

Windows needs the WebView2 runtime, which ships with Windows 11 and with any recent Edge. Linux needs WebKitGTK, libwebkit2gtk-4.1-0 on Debian and Ubuntu, which most desktop installs already carry.

The same bundled page serves all three. Where the browser has a barcode detector of its own, Chrome on Android and macOS among them, the page uses it; elsewhere it decodes with zxing-cpp compiled to WebAssembly, in a worker where the page can start one, so on the web the decoding stays off the app's thread. On the desktop the plugin answers the page's camera permission request on the host side. That last part is what usually stops a webview from scanning on Linux: WebKitGTK denies a media request the embedder does not handle.

Install #

flutter pub add universal_barcode_scanner

Requires Flutter 3.35 or later.

Android #

Camera permission goes in your own manifest, android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />

The plugin asks for minSdkVersion 21 and builds against compileSdk 36, the level two of its dependencies, flutter_plugin_android_lifecycle and webview_all_android, already require. An app has to compile against the highest of them:

android {
    compileSdk = 36
}

iOS #

A usage description goes in ios/Runner/Info.plist. iOS kills the app without it:

<key>NSCameraUsageDescription</key>
<string>Camera permission is required for barcode scanning.</string>

Windows #

Nothing to declare, but if you build with Visual Studio 2026 or later the build fails on error STL1011 from <experimental/coroutine>. That comes from the Windows Implementation Library pinned by the webview dependency, not from this plugin, and both Windows webviews on pub.dev pin the same 2022 release. Until it is bumped upstream, add this to your app's windows/CMakeLists.txt, above add_subdirectory(${FLUTTER_MANAGED_DIR}):

add_compile_definitions(_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS)

macOS #

Two things, and the scanner fails silently without either. A usage description in macos/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera permission is required for barcode scanning.</string>

And the camera entitlement in both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements, since a macOS app is sandboxed:

<key>com.apple.security.device.camera</key>
<true/>

Requires macOS 10.15 or later.

Web #

Browsers only hand out the camera on a secure origin, so the page must be served over HTTPS. localhost counts as secure during development.

Scan one code #

Opens the scanner as a route and comes back with what it read, or null if the user backed out:

final String? code = await UniversalBarcodeScanner.scan(context);

scanResult does the same and says which symbology the code was printed in:

final ScanResult? result = await UniversalBarcodeScanner.scanResult(context);
if (result?.format == BarcodeFormat.ean13) {
  // A product.
}
Parameter Default Effect
lineColor Color(0xFFFF6666) Colour of the scan line, on every platform.
scanFormat ScanFormat.all Symbologies to accept, on every platform. Fewer formats also scan faster.
scanWindow ScanWindow.wide Shape of the scan window: wide for barcodes, square for QR codes, or none to read the whole frame with nothing drawn over it.
scanWindowSize null Size of the scan window in logical pixels, or one picked from scanWindow. On every platform.
cameraFace CameraFace.back Which camera to open.
showTorchButton false Whether the native torch button is shown, when the camera has a torch. Android and iOS.
bar null A ScannerBar above the scanner. Without one, a close button sits over the camera. Web, Windows and Linux, except its cancelLabel, which labels the native cancel button.
child null Drawn over the scanner, for instance a manual entry field. Web, Windows and Linux.
backgroundColor black Colour around the camera. Web, Windows and Linux.
flip null Mirrors the camera left to right. null mirrors it where it is a webcam facing the user, on a desktop and in a desktop browser, and not on a phone or a tablet. Web, Windows and Linux.
flipVertical false Shows the camera upside down. Web, Windows and Linux.
buttons none Buttons over the camera, from ScannerButton: torch, pause, flipHorizontal, flipVertical, zoom, switchCamera. Web, Windows and Linux; torch also shows the native torch button on Android and iOS.
labels ScannerLabels.english The words of the buttons, for screen readers, and of the page when the camera will not start. ScannerLabels.french, .dutch and .german are ready to use, and copyWith changes any of them. Web, Windows and Linux.
animate true Fades the camera in when it starts and turns it over when flipped. Off when the platform asks for reduced motion. Web, Windows and Linux.
buttonsAlignment Alignment.centerRight Where the buttons sit. Centred on the left or the right, they run down the side; elsewhere, across.
buttonStyle round and dark A ScannerButtonStyle: the size, spacing, colours off and on, focus ring, corners and tooltips of the buttons and of the close button. Web, Windows and Linux.
vibrate false A short vibration for each code read, where the device has one. On every platform.
beep false A short beep for each code read: a tone on Android, iOS and the web, the system's alert sound on a desktop. On every platform.

The buttons work with the mouse and the keyboard as well as by touch: the hand shows over them, a tooltip says what each one does, Tab moves between them with a ring around the one reached, and Enter or Space presses it.

UniversalBarcodeScanner.scan(
  context,
  buttons: const <ScannerButton>{ScannerButton.torch, ScannerButton.zoom},
  buttonStyle: const ScannerButtonStyle(
    size: 52,
    backgroundColor: Color(0xCCFFFFFF),
    foregroundColor: Color(0xFF0E1014),
    activeBackgroundColor: Color(0xFF2E9E6A),
    borderRadius: BorderRadius.all(Radius.circular(10)),
  ),
);

ScannerButtonStyle takes:

Parameter Default Effect
size 44 Width and height of a button, in logical pixels. 44 is the least a finger reliably hits.
iconSize 20 Size of the icon inside it.
spacing 8 Space between two buttons of the group.
backgroundColor translucent black Fill of a button whose setting is off.
foregroundColor white Icon of a button whose setting is off.
activeBackgroundColor near-opaque white Fill of a button whose setting is on: the torch lit, reading paused, a flip applied, a zoom.
activeForegroundColor black Icon of a button whose setting is on.
focusColor white Ring drawn around the button the keyboard is on, a little outside it.
borderRadius null Corners of a button, or a circle when null.
showTooltips true Whether a button says what it does when the mouse rests on it or the keyboard reaches it.

copyWith changes some of them and keeps the rest.

Android, iOS and macOS open a native screen over the route, so the parameters that shape the Flutter page do nothing there. Escape closes the scanner on macOS, the web, Windows and Linux, and popping the route from your own code closes it on every platform.

When the camera cannot be used #

On Android, iOS and macOS, scan throws a ScannerException and closes the route itself:

try {
  final String? code = await UniversalBarcodeScanner.scan(context);
} on ScannerException catch (error) {
  switch (error.code) {
    case ScannerErrorCode.permissionDenied: // the user refused the camera
    case ScannerErrorCode.cameraUnavailable: // no camera, or it did not start
    case ScannerErrorCode.alreadyActive: // another scanner is on screen
    case ScannerErrorCode.unknown:
  }
}

The scanner shows no dialog of its own: what to tell the user is the app's call. On the web, Windows and Linux the scanner page says why the camera did not start, and the user backs out.

Keep scanning #

Same route, but every code read is emitted instead of the first one closing it. The stream closes by itself when the route goes away, whichever way it goes, and cancelling the subscription closes the route:

final StreamSubscription<String> sub =
    UniversalBarcodeScanner.stream(context).listen((String code) {
  debugPrint(code);
});

A code held in front of the camera is emitted once, and again only after it has been out of sight for a second. scanDelay adds a least time between any two codes. A camera that cannot be used is emitted as a ScannerException, then the stream closes.

stream takes the same parameters as scan, plus scanDelay. resultStream emits a ScanResult for each code, with its symbology.

Embed the camera #

To put the camera inside your own layout rather than on its own route, on every platform:

The camera opening inside a tile of the example app, on Android

UniversalBarcodeScanner(
  continuous: true,
  onScanned: (String code) => debugPrint(code),
  onError: (ScannerException error) => debugPrint('$error'),
  onCreated: (ScannerController controller) {
    this.controller = controller;
  },
);

The view asks for the camera permission itself and only reads codes that sit entirely inside its scan window. The camera fills the view, cropped rather than letterboxed, and the scan window is drawn the same way everywhere. On Android and iOS the camera also stops while the app is in the background.

The controller drives the running camera:

final bool torchOn = await controller.toggleFlash();
await controller.pauseScanning();
await controller.resumeScanning();
final double zoom = await controller.setZoom(2);

isPaused, isTorchOn and zoom on the controller say how the view stands, whoever changed it: the app, the buttons over the camera, or the view pausing itself after a code. The buttons show the same state.

A paused view keeps its camera running and its scan line stops where it is, until resumeScanning. A view that is not continuous pauses the same way on its first code.

toggleFlash answers false where there is no torch to drive: a Mac camera, and most webcams, which browsers and webviews give a page no control over. setZoom answers with the zoom the camera applied, 1 where it cannot zoom, which is the case of the same cameras.

Parameter Default Effect
onCreated required Called once the view exists, with its ScannerController.
onScanned null Called with every code read.
onResult null Called with every code read, as a ScanResult with its symbology.
onError null Called when the camera cannot be used. On the web, Windows and Linux the view also says why.
continuous false When false, the view pauses on the first code until resumeScanning.
scanWindowSize null Size of the scan window in logical pixels, or one picked from scanWindow.
lineColor, scanWindow, cameraFace, scanFormat, scanDelay, flip, flipVertical, buttons, buttonsAlignment, buttonStyle, animate, labels, vibrate, beep, child see above As in scan and stream, on every platform: the view is drawn by Flutter everywhere, so its buttons are too. On Android, iOS and macOS the native view says when its first frame is on screen, and the camera fades in then.

The view fills the constraints it is given. What to scan is read once, when the view is created: give the widget a new key to change it. The flip is the exception: it follows the widget, and the camera keeps running. The callbacks are always the current widget's, and none is called once the widget is gone.

Scanner bar #

Passing a ScannerBar puts a bar above the web, Windows and Linux scanner. Its back button is on unless you turn it off, since on a desktop it is the way out:

UniversalBarcodeScanner.scan(
  context,
  bar: const ScannerBar(
    title: 'Scan',
    backIcon: Icon(Icons.arrow_back_ios),
  ),
);
Parameter Default Effect
title null Text of the bar, or none.
centerTitle false Centres the title rather than starting it after the back button.
showBackButton true Shows the back button, which closes the scanner.
backIcon a chevron Icon of the back button. An Icon takes foregroundColor.
backgroundColor black Colour of the bar.
foregroundColor white Colour of the title and of the back icon, and of the ring around the back button when the keyboard is on it.
cancelLabel 'Cancel' The text of the cancel button of the native screens of Android, iOS and macOS, and what a screen reader says for the back button of the bar.

On Android, iOS and macOS the native screen draws its own controls and has no bar: only cancelLabel reaches it.

Migrating from 1.x #

The breaking changes of 2.0 and what to write instead are listed at the top of the changelog.

Example #

example/ is one screen with the three ways to scan, and what each one returns.

cd example && flutter run

Tests #

flutter test

The camera page the web, Windows and Linux scanners run has its own tests, in a browser with a camera drawn on a canvas. They need Node:

cd tool/page_test
npm ci
npx playwright install chromium
npx playwright test

Dependencies #

webview_all for the Windows and Linux scanners, and web for the iframe on the web. Android carries CameraX and the ML Kit barcode model, which is bundled, so the scanner works on a device with no Play services and downloads nothing on first use. The Android code is Kotlin: under AGP 9 it uses the built-in Kotlin support, and under earlier versions the Kotlin plugin every Flutter app already has. Nothing on iOS or macOS beyond the SDKs.

No design system. The package is written against package:flutter/widgets.dart alone, so it sits under Material, under material_ui, or under neither, and imposes none of them on your app. The scanner bar is drawn here rather than taken from a widget library; ScannerBar carries its colours.

License #

MIT, see LICENSE.

The web, Windows and Linux scanner bundles barcode-detector and zxing-wasm, both MIT, and zxing-cpp, which zxing-wasm compiles and which is Apache 2.0. The licences travel with the files, in lib/assets/THIRD_PARTY_LICENSES.txt.

1
likes
0
points
1.39k
downloads

Publisher

verified publishercomapps.be

Weekly Downloads

Barcode and QR code scanner for Flutter, on Android, iOS, Linux, macOS, web and Windows, from one widget and one callback.

Homepage
Repository (GitHub)
View/report issues

Topics

#barcode #qrcode #scanner #camera #reader

License

unknown (license)

Dependencies

flutter, flutter_plugin_android_lifecycle, web, webview_all

More

Packages that depend on universal_barcode_scanner

Packages that implement universal_barcode_scanner