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
BarcodeDetectorAPI 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=trueinandroid/gradle.propertiesfor ~600 KB plus an on-demand model download. autoZoomis 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 asqr_code_scannercover 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.