Open-source project
juliansteenbakker/mobile_scanner avatar
juliansteenbakker/mobile_scanner

mobile_scanner, Flutter barcode scanning with an honest feature matrix

A universal Flutter barcode and QR code scanner using CameraX/ML Kit for Android, AVFoundation/Apple Vision for iOS & macOS, and ZXing for web.

1,176 stars719 forksDartBSD-3-Clause

At a glance

What is it?
mobile_scanner wraps three different native scanning stacks behind one Dart API, ML Kit with CameraX on Android, AVFoundation and Apple Vision on iOS and macOS, and ZXing on the web, and it documents in a table exactly which of its features exist where. The parts worth reading closely are the two lens helpers, which answer a question the underlying engines answer in four different ways, and the Android size decision between a bundled and a downloaded ML Kit.
Who is it for?
The plugin fits a Flutter team that needs barcode scanning on mobile and web from one API and who is willing to read the platform matrix, because the web implementation is a different engine with fewer capabilities and the lens helpers behave differently on every platform. It does not fit a project that needs Linux or Windows, that needs to analyse a captured image on the web, or that cannot accept the three to ten megabytes the bundled Android ML Kit adds.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly Dart, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One Dart API over three native scanning stacks

The package description names the engines, and that is the most useful thing in it. It is a universal Flutter barcode and QR code scanner using CameraX with ML Kit for Android, AVFoundation and Apple Vision for iOS and macOS, and ZXing for the web.

That is a difficult thing to do well, because the three engines are genuinely different products. ML Kit is a Google machine learning library with a bundled or downloadable model. AVFoundation is the Apple media framework, with barcode detection exposed through Apple Vision, and its capabilities depend on the operating system version. ZXing is a cross-platform Java barcode library that the web implementation loads as a JavaScript port from a content delivery network.

The Dart API above them covers the common ground: fast barcode and QR code scanning, support for multiple barcode formats, real-time detection, and customisation of the camera and the scanner behaviour. The examples directory carries runnable versions of the typical cases, including the basic usage, applying a scan window, and retrieving images from the barcodes.

The platform table sets the boundary, and it is worth reading as a list of what exists rather than what does not. Android, iOS, macOS and the web are supported. Linux and Windows are not, with a cross in each cell, which is a reasonable line for a camera plugin but a hard one for a team that ships a Flutter desktop application.

The plugin is published on pub.dev, licensed under BSD-3-Clause, and developed on a develop branch rather than on main, which is the normal arrangement for a Flutter package that generates platform code during the build.

The feature matrix is the most useful documentation

Below the platform table there is a second table, and it is the one to read before writing any code.

The features listed are analyzeImage, which analyses a still image, returnImage, which returns the image behind a detected barcode, scanWindow, which restricts detection to a region of the preview, autoZoom, lensType, getSupportedLenses with a facing filter, and getBestCloseRangeScanningLens.

On Android, iOS and macOS the first three are all available. On the web all three are marked as unsupported, and so are autoZoom and the lens queries. The reason is the engine, and the readme says it in the lens section: the MediaDevices API has no concept of lens type, and its focusDistance capability, where available at all, is limited to Chrome on Android.

Two of the columns are more specific. autoZoom is available on Android alone. lensType is available on Android and iOS but not on macOS. And getBestCloseRangeScanningLens has a footnote per platform: on Android it is always the normal lens, on iOS it requires iOS 15 and falls back to the normal lens, and on macOS and the web it is always the normal lens.

Those annotations are the difference between a plugin that works and a plugin that works on your platform. A table of checkmarks would have hidden the fact that the web implementation is a reduced version of the mobile one, and a cross in the autoZoom cell for macOS would not have told you that iOS has it while macOS does not.

The readme points at the example app for the detailed implementation information, which is where the matrix becomes code.

getSupportedLenses, and the front and back camera mix-up

The first lens helper answers a simple question, which is which cameras does this device have. The subtlety is in how the answer is filtered.

dart
// All cameras (back + front)
final Set<CameraLensType> allLenses = await controller.getSupportedLenses();

// Back cameras only
final Set<CameraLensType> backLenses = await controller.getSupportedLenses(
  facing: CameraFacing.back,
);

if (backLenses.contains(CameraLensType.zoom)) {
  // Device has a telephoto back camera
}

Without a facing filter, the results may include lenses from both the front and the back cameras, and the readme says plainly that this can cause incorrect lens-type detection when switching cameras. That is a specific bug being designed out: a device with a telephoto back camera and a wide front camera reports the union, and a caller that checks for the zoom lens without restricting the facing direction can be misled by a front camera's characteristics.

The filter is supported on Android and iOS. On macOS and the web it is ignored, and all lenses are returned, which is worth knowing before writing code that depends on the filter behaving.

The camera lens type set is what makes the check meaningful. A device that reports the zoom type has a telephoto back camera, which is the case the caller is usually trying to detect, since a telephoto lens is the one that can focus from far enough away to read a barcode on a shelf.

getBestCloseRangeScanningLens answers the same question four ways

The second helper is the more interesting one, because it is the one that has to be implemented on top of four different platform capabilities and cannot give the same answer everywhere.

The intent is to find the lens that can focus closest to the phone, which is therefore the one best suited to scanning a barcode held at close range. The readme is careful about the contract: it returns null if the device has no camera for the given facing direction, and the returned lens type is not guaranteed to be available, which is why it should be combined with getSupportedLenses before switching.

dart
final bestLens = await controller.getBestCloseRangeScanningLens(facing: CameraFacing.back);
final supported = await controller.getSupportedLenses(facing: CameraFacing.back);

if (bestLens != null && supported.contains(bestLens)) {
  await controller.switchCamera(
    SelectCamera(facingDirection: CameraFacing.back, lensType: bestLens),
  );
}

What it returns differs by platform, and the readme lists every case. On iOS 15 and later it is the camera with the shortest AVCaptureDevice.minimumFocusDistance, typically the ultra-wide lens on newer iPhones with macro autofocus at about two centimetres, or the standard 1x camera on older models. On iOS below 15 and on macOS it is the normal lens, since minimumFocusDistance is not available there. On Android it is the normal lens whenever the device has a camera, because LENS_INFO_MINIMUM_FOCUS_DISTANCE is only meaningful on physical sub-cameras which, like the rest of getSupportedLenses, cannot be selected independently through CameraX, so the main camera, which has the most capable autofocus on virtually all Android devices, is returned unconditionally. On the web it is again the normal lens, because the MediaDevices API has no concept of lens type. On all platforms it is null if there is no camera for the requested direction.

So the helper is a genuine improvement on iOS 15 and later, and a constant everywhere else. A caller that switches cameras based on it will find it does nothing on Android, and that is by design rather than a bug.

The Android size decision is a properties file

The configuration section has one decision that matters for the size of your application, and it is a good example of a plugin exposing a genuine trade-off.

By default the package uses the bundled version of ML Kit barcode scanning for Android. That version is immediately available to the device, and it increases the size of the app by approximately three to ten megabytes. The alternative is the unbundled version, which is downloaded on first use via Google Play Services and increases the app size by around 600 kilobytes.

Switching is a single line in the android/gradle.properties file:

properties
dev.steenbakker.mobile_scanner.useUnbundled=true

The trade is between an application that scans immediately after install and one that downloads the model on first use. The bundled version is a better experience and a worse download; the unbundled version is a smaller package and a first run that waits for a download, with the dependency on Google Play Services being present and the model not.

For an application where scanning is the primary function, the bundled version is usually right, since the size is a one-off cost against the first scan being instant. For an application where the scanner is one of several features, the 600 kilobyte version is the sensible default.

The rest of the configuration is the platform permissions. On iOS the scanner needs the camera, so NSCameraUsageDescription has to be added to the Info.plist with a description of why the application needs camera access, and if the local gallery feature from image_picker is used, NSPhotoLibraryUsageDescription as well. The readme gives the key and string pairs, and the macOS section follows the same pattern for the camera entitlement.

The web implementation pins its libraries from a CDN

One detail in the repository explains how the web implementation is kept honest, and it is unusual enough to be worth reading on its own.

The web version of the plugin loads its barcode libraries from a content delivery network, and the versions of those libraries are pinned in a small private package.json at the repository root, named mobile-scanner-web-dependencies. Its description says it exists so that Dependabot can detect and propose updates, and that it is kept in sync with lib/src/web/web_library_versions.dart by a test, test/web/web_library_versions_test.dart.

The two dependencies are @zxing/library at version 0.23.0 and zxing-wasm at 3.1.3. The package is private, so it is never published, and it exists purely as a place where a dependency tool can see the versions that are actually in use.

That is a small piece of engineering with a clear purpose. Without it, the versions loaded by the web build would live in a Dart constant where no dependency bot would look, and a security update in ZXing would arrive silently in the form of an outdated script tag. With it, Dependabot proposes a bump, and a test fails if the Dart constant and the package manifest disagree, so the two cannot drift.

The rest of the repository follows the standard layout for a Flutter plugin, with android/ and darwin/ holding the platform implementations, lib/ the Dart code, example/ a full application with its own per-platform directories and screenshots, test/ the tests, and a pubspec.yaml, an analysis_options.yaml, a release-please configuration for automated releases, a CHANGELOG.md and a SECURITY.md.

Where mobile_scanner is the wrong tool

The first limit is the platform list. Linux and Windows are not supported, and a Flutter project that ships a desktop application cannot use this plugin for its scanning features.

The second is the web reduction. analyzeImage, returnImage and scanWindow are unavailable on the web, and so is autoZoom. A product whose primary flow is scanning a barcode from a photo the user already has cannot use the image analysis path there, and the feature matrix simply marks those cells as unsupported for the web.

The third is the Android size cost, which for a large application with a small download budget is not a rounding error. The bundled ML Kit is the default, and the alternative requires the user to tolerate a first-use download and a dependency on Google Play Services.

The fourth is that the lens helpers are only genuinely useful on one platform. getBestCloseRangeScanningLens returns a real answer on iOS 15 and later, and a constant everywhere else, so an application that treats it as a universal capability will ship a camera switch that does nothing on Android and a claim in its documentation that is true for one platform. The readme is honest about this, and an application built on top of it has to be honest too.

The fifth is the versioning. The releases move quickly, with 7.4.0, 7.4.1 and 7.4.2 published between July and September 2026, and the development happens on a develop branch, so a team that wants stability has to pin a version and check the changelog, and a team that wants the newest behaviour has to follow a branch. Neither is a problem, but both are decisions.

Editorial conclusion

The plugin fits a Flutter team that needs barcode scanning on mobile and web from one API and who is willing to read the platform matrix, because the web implementation is a different engine with fewer capabilities and the lens helpers behave differently on every platform. It does not fit a project that needs Linux or Windows, that needs to analyse a captured image on the web, or that cannot accept the three to ten megabytes the bundled Android ML Kit adds. Before adopting it, decide whether Android should ship the bundled or the unbundled scanner, since the difference is a gradle.properties line and several megabytes, and treat getBestCloseRangeScanningLens as an iOS 15 feature, since Android and the web always return the normal lens. The licence is BSD-3-Clause, version 7.4.2 was published on 2026-09-10, and the last push was on 2026-10-14.

Frequently asked questions

Which platforms does mobile_scanner support?

Android, iOS, macOS and the web. Linux and Windows are marked as not supported in the platform table. The scanning engines differ by platform, with CameraX and ML Kit on Android, AVFoundation and Apple Vision on iOS and macOS, and ZXing on the web.

Which mobile_scanner features are not available on the web?

analyzeImage, returnImage, scanWindow, autoZoom and the lens queries are all unavailable on the web. The lens helpers and autoZoom are marked as unsupported on the web; the readme explains that the MediaDevices API has no concept of lens type.

How do I reduce the size of my Android app when using mobile_scanner?

Set `dev.steenbakker.mobile_scanner.useUnbundled=true` in your android/gradle.properties file. That switches the plugin to the unbundled ML Kit barcode scanner, which is downloaded on first use through Google Play Services and adds around 600KB, instead of the bundled version that adds roughly three to ten megabytes.

What does getBestCloseRangeScanningLens return on Android?

Always the normal lens, when the device has a camera. The minimum focus distance is only meaningful on physical sub-cameras, which cannot be selected independently through CameraX, so the main camera is returned unconditionally. On iOS 15 and later it returns the camera with the shortest minimum focus distance.

Which iOS usage descriptions do I need for mobile_scanner?

NSCameraUsageDescription, describing why the application needs camera access, and NSPhotoLibraryUsageDescription as well if you use the local gallery feature from the image_picker package. The macOS configuration follows the same pattern for the camera entitlement.

Official sources

  1. juliansteenbakker/mobile_scanner on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/juliansteenbakker-mobile-scanner.svg)](https://hysenlabs.com/projects/juliansteenbakker-mobile-scanner)

Was this useful?