Skip to content
Blog

QR Code Scanning in Flutter with mobile_scanner

Build a production-ready QR scanner in Flutter with mobile_scanner: permissions, controller configuration, lifecycle handling, and deduplication.

Published on • November 2, 2026

AI Assistant

Scanning QR codes is table stakes for ticketing, payments, and inventory apps, and mobile_scanner (7.5.0) is the universal scanner built on CameraX/ML Kit (Android), AVFoundation/Apple Vision (iOS, macOS), and ZXing on web — supporting Android, iOS, macOS, and Web, but not Linux or Windows (pub.dev).

A Minimal Scanner

Add mobile_scanner: ^7.5.0, then provide NSCameraUsageDescription in iOS Info.plist, the camera permission in AndroidManifest.xml, and an HTTPS origin on web. The feed itself is one widget; onDetect hands you a BarcodeCapture, often with several barcodes:

MobileScanner(
  onDetect: (BarcodeCapture capture) {
    final value = capture.barcodes.firstOrNull?.rawValue;
    if (value != null) debugPrint('Scanned: $value');
  },
)

Configuring Formats, Detection Speed, and Torch

final controller = MobileScannerController(
  formats: const [BarcodeFormat.qrCode],
  detectionSpeed: DetectionSpeed.noDuplicates, // suppresses repeat frames
  facing: CameraFacing.back,
  autoTorch: false,
);
await controller.toggleTorch(); // or switchCamera() / analyzeImage(path)

Lifecycle: Pause When Backgrounded

Pass it as MobileScanner(controller: controller, ...), then use autoStart: false with a WidgetsBindingObserver — registered in initState, removed alongside controller.dispose() in dispose() — so the stream stops behind other apps:

class ScannerState extends State<Scanner> with WidgetsBindingObserver {
  final controller = MobileScannerController(autoStart: false);

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      unawaited(controller.start());
    } else {
      unawaited(controller.stop()); // inactive, paused, hidden, detached
    }
  }
}

Deduplicating Rapid onDetect Hits

At 30+ frames per second the same QR code fires onDetect dozens of times. DetectionSpeed.noDuplicates handles the steady case; add your own cooldown before navigating or mutating state, and reach for scanWindow (restrict detection to a mid-frame rectangle) or returnImage/analyzeImage (fetch the bitmap) instead of running full-frame detection:

String? _last;
var _at = DateTime.fromMillisecondsSinceEpoch(0);
bool accept(String? v) {
  final fresh = v != null &&
      (v != _last || DateTime.now().difference(_at) > const Duration(seconds: 2));
  if (fresh) { _last = v; _at = DateTime.now(); }
  return fresh;
}

Gotchas, Web Caveats, and Alternatives

  • Web uses the native BarcodeDetector API with a zxing-wasm fallback: expect slower first detection and start scanning from a user gesture.
  • ML Kit is bundled by default (+3–10 MB); set dev.steenbakker.mobile_scanner.useUnbundled=true in android/gradle.properties for ~600 KB plus an on-demand model download.
  • autoZoom is Android-only, and there is no Linux/Windows support — use USB scanners or a web build there.
  • For rendering codes use qr_flutter; older plugins such as qr_code_scanner cover far fewer platforms.

Configure the controller for your formats and detection speed, stop it on lifecycle changes, and deduplicate onDetect — that is the line between a demo and a shippable scanner.