qr_scanner_fast 0.0.2
qr_scanner_fast: ^0.0.2 copied to clipboard
A low-latency hybrid QR scanner that races ZXing with an ML Kit fallback.
qr_scanner_fast #
qr_scanner_fast is a low-latency QR scanner for Flutter that uses native camera
pipelines and several decoders in parallel. It is designed for cases where a QR
code may be small, near the edge of the camera preview, moving, rotated, or only
visible for a few frames.
The scanner analyzes the entire camera image. The on-screen cutout is only a visual guide, so a code can be detected before it reaches the center box.
Why this package exists #
A conventional scanner often sends one frame at a time through one decoder. If that decoder is busy, a short-lived frame can be dropped before it is checked. That becomes noticeable when the user moves the phone or QR code quickly.
This package takes a different approach:
- Camera capture and preview run natively, outside the Dart UI isolate.
- Live decoding and historical snapshot decoding are separate pipelines.
- Recent sampled frames are kept temporarily in a bounded in-memory buffer.
- Multiple decoders race; the first valid result wins.
- The full image is scanned instead of cropping to the overlay.
- Candidate regions can be sent to a smaller ZXing job for another decode attempt.
No snapshot is written to disk. Historical frames exist only in memory and the buffer has a fixed maximum size.
Platform support #
| Platform | Minimum version | Camera pipeline | Decoders |
|---|---|---|---|
| Android | API 24 | CameraX | ZXing and Google ML Kit |
| iOS | 15.5 | AVFoundation | Apple Vision, Google ML Kit, and ZXing ROI fallback |
The package requires Flutter 3.38 or newer and Dart 3.10 or newer. Web, macOS, Windows, and Linux are not supported.
Android uses Java 17, CameraX 1.5.3, Google ML Kit
How it works #
Shared pipeline #
flowchart TD
C[Native camera stream] --> P[Preview]
C --> S{Frame sampler}
S --> L[Live pipeline]
S --> H[Bounded historical snapshot buffer]
L --> R{First valid QR result}
H --> R
R --> D[Dart onDetect callback]
style P fill:#183153,color:#fff
style R fill:#176b3a,color:#fff
The preview does not wait for decoding. Sampling starts as soon as the camera starts, whether or not a QR code has already been located.
Android pipeline #
flowchart LR
F[CameraX YUV frame] --> Z[Live ZXing worker]
F -->|after fallbackDelay| M[Live ML Kit]
M -->|decoded| W[Winner]
M -->|potential bounding box| ROI[Priority ZXing ROI]
Z --> W
ROI --> W
F --> B[Sharpness-filtered snapshot buffer]
B --> SZ[Snapshot ZXing worker]
B --> SM[Snapshot ML Kit worker]
SZ --> W
SM --> W
CameraX targets a 1280×720 analysis stream, uses keep-only-latest backpressure, requests continuous autofocus and a 30 FPS range when supported, and lets ML Kit suggest zoom for a QR that is visible but too small to decode.
iOS pipeline #
flowchart LR
F[AVFoundation pixel buffer] --> V[Live Apple Vision]
F -->|after fallbackDelay| M[Live ML Kit]
V -->|decoded| W[Winner]
V -->|candidate region| R[Dart ZXing isolate]
R --> W
M --> W
F --> B[Bounded snapshot ring]
B --> SV[Snapshot Vision]
B --> SM[Snapshot ML Kit]
SV --> W
SM --> W
SV -->|candidate region| R
Only a grayscale candidate crop is transferred to Dart for ZXing. Full camera frames remain native.
Installation #
Add the package to the application's pubspec.yaml. While developing locally:
dependencies:
qr_scanner_fast: ^0.0.2
Then run:
flutter pub get
Android setup #
Set the application minimum SDK to 24 or newer:
// android/app/build.gradle.kts
android {
defaultConfig {
minSdk = 24
}
}
Ensure camera permission is present in
android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<application ... />
</manifest>
The plugin requests runtime camera permission when its native view is created.
iOS setup #
Set the deployment target to iOS 15.5 or newer and add a camera usage message to
ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>The camera is used to scan QR codes.</string>
This plugin currently integrates its native ML Kit dependency through
CocoaPods. Run flutter pub get before pod install when installing pods
manually:
cd ios
pod install
Basic usage #
import 'package:flutter/material.dart';
import 'package:qr_scanner_fast/qr_scanner_fast.dart';
class ScanPage extends StatelessWidget {
const ScanPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: QrScannerFast(
onDetect: (result) {
debugPrint('Value: ${result.rawValue}');
debugPrint('Decoder: ${result.engine.name}');
},
onError: (error, stackTrace) {
debugPrint('Scanner error: $error');
},
),
);
}
}
The default overlay is a guide only. Pass overlay: null to remove it or supply
a custom widget:
QrScannerFast(
overlay: const QrScannerOverlay(
cutoutSize: 280,
borderColor: Colors.greenAccent,
overlayColor: Color(0x77000000),
),
onDetect: (result) {},
)
Controller and repeated scanning #
Use QrScannerFastController for pause, resume, torch, and starting a new
scan after stopAfterDetection stops the scanner:
class ScannerPage extends StatefulWidget {
const ScannerPage({super.key});
@override
State<ScannerPage> createState() => _ScannerPageState();
}
class _ScannerPageState extends State<ScannerPage> {
final _controller = QrScannerFastController();
String? _value;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
QrScannerFast(
controller: _controller,
onDetect: (result) => setState(() => _value = result.rawValue),
),
SafeArea(
child: Row(
children: [
IconButton(
onPressed: _controller.toggleTorch,
icon: const Icon(Icons.flash_on),
),
if (_value != null)
FilledButton(
onPressed: () async {
setState(() => _value = null);
await _controller.start();
},
child: const Text('Scan again'),
),
],
),
),
],
);
}
}
Controller methods:
| Method | Behavior |
|---|---|
pause() |
Pauses frame analysis. The native camera preview may remain allocated. |
resume() |
Resumes analysis after pause(). |
toggleTorch() |
Toggles the rear-camera torch when available. |
start() |
Clears the stopped state and starts another scan. |
status and torchEnabled are listenable properties because the controller
extends ChangeNotifier.
Configuration #
const config = QrScannerFastConfig(
fallbackDelay: Duration(milliseconds: 120),
frameSampleRate: 1,
snapshotFrameSampleRate: 4,
snapshotBufferSize: 6,
minimumSnapshotSharpness: 3.5,
frameInterval: Duration(milliseconds: 45),
mlKitFrameInterval: Duration(milliseconds: 250),
maxZxingDimension: 720,
duplicateCooldown: Duration(seconds: 2),
stopAfterDetection: true,
tryInverted: false,
);
| Option | Default | Purpose |
|---|---|---|
fallbackDelay |
120 ms | Delay before ML Kit joins the live race. |
frameSampleRate |
1 | Analyze every Nth live frame. 1 means every frame. |
snapshotFrameSampleRate |
4 | Capture one historical in-memory snapshot every N frames. |
snapshotBufferSize |
6 | Maximum queued historical frames; native code caps this at 24. |
minimumSnapshotSharpness |
3.5 | Android sharpness threshold. Use 0 to disable the filter. |
frameInterval |
45 ms | Minimum interval between Android live ZXing attempts. |
mlKitFrameInterval |
250 ms | Minimum interval between live ML Kit attempts. |
maxZxingDimension |
720 | Maximum dimension of an iOS ROI decoded by the Dart ZXing isolate. |
duplicateCooldown |
2 s | Suppresses repeated Dart callbacks for the same value. |
stopAfterDetection |
true |
Stops analysis after the first result until start() is called. |
tryInverted |
false |
Also tries light QR modules on a dark background with ZXing. |
Practical tuning guidance:
- Start with the defaults and test on the slowest supported phone.
- Use
fallbackDelay: Duration.zerowhen fastest possible first detection is more important than CPU usage. - Increase
frameSampleRateorframeIntervalif the device becomes hot. - A larger snapshot buffer can retain more missed moments, but consumes more memory and may increase the time spent checking old frames.
- Increase
maxZxingDimensionfor very small iOS candidates only when the additional CPU cost is acceptable.
Scan results #
QrScanResult contains:
rawValue: decoded QR payload.engine:QrScannerEngine.zxing,mlKit, orvision.timestamp: time at which the result reached Dart.
When several workers decode the same QR almost simultaneously, the first valid
result is emitted and subsequent results are suppressed according to scanner
state and duplicateCooldown.
Standalone ZXing luminance decoder #
The Dart ZXing utility can decode a tightly packed, row-major 8-bit grayscale image without displaying the camera widget. Decoding runs outside the UI isolate:
final value = await ZxingQrDecoder.decodeLuminance(
bytes: grayscaleBytes,
width: imageWidth,
height: imageHeight,
tryInverted: true,
);
The byte buffer must contain at least width * height bytes.
Lifecycle and disposal #
- Remove
QrScannerFastfrom the widget tree to release its platform view and native camera resources. - If the application creates a
QrScannerFastController, it must callcontroller.dispose()from the owning widget'sdispose()method. pause()pauses analysis but is not a replacement for disposing the widget.- Native snapshot queues are bounded and cleared when scanning stops or the native view is disposed.
Limitations #
- A QR code must be visible to the camera sensor for at least one captured frame. No scanner can decode content that never enters the image.
- Motion blur, poor focus, glare, damaged quiet zones, and a QR code containing too few pixels can still prevent decoding.
- Automatic zoom support depends on the Android camera and ML Kit suggestions.
- The package scans QR codes only; other barcode formats are intentionally disabled.
- iOS integration currently uses CocoaPods rather than Swift Package Manager.
Example and validation #
The complete runnable application is available in
example/lib/main.dart.
Run the package checks with:
flutter analyze
flutter test
cd example
flutter run