qr_scanner_fast 0.0.2 copy "qr_scanner_fast: ^0.0.2" to clipboard
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.zero when fastest possible first detection is more important than CPU usage.
  • Increase frameSampleRate or frameInterval if 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 maxZxingDimension for very small iOS candidates only when the additional CPU cost is acceptable.

Scan results #

QrScanResult contains:

  • rawValue: decoded QR payload.
  • engine: QrScannerEngine.zxing, mlKit, or vision.
  • 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 QrScannerFast from the widget tree to release its platform view and native camera resources.
  • If the application creates a QrScannerFastController, it must call controller.dispose() from the owning widget's dispose() 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
1
likes
150
points
125
downloads

Documentation

API reference

Publisher

verified publishersaddamnur.xyz

Weekly Downloads

A low-latency hybrid QR scanner that races ZXing with an ML Kit fallback.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, zxing2

More

Packages that depend on qr_scanner_fast

Packages that implement qr_scanner_fast