import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ScanPage extends StatelessWidget { const ScanPage({super.key, required this.title}); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: ScanView( resultHandler: (capture, resumeScanner) { context.pop(capture.barcodes.first.rawValue); }, ), ); } } typedef ScanViewResultHandler = void Function( BarcodeCapture capture, void Function() resumeScanner, ); class ScanView extends StatefulWidget { const ScanView({super.key, required this.resultHandler}); /// handle the QR code result. /// /// The scanner will stop each time the result is complete. /// Process the result here and then resume the scanner by calling `resumeScanner`. final ScanViewResultHandler resultHandler; @override State createState() => _ScanViewState(); } class _ScanViewState extends State with WidgetsBindingObserver { final scannerController = MobileScannerController( formats: [BarcodeFormat.qrCode], ); StreamSubscription? subscription; bool hasResult = false; void resumeScanner() { hasResult = false; scannerController.start(); } void handleQRcode(BarcodeCapture capture) { if (hasResult) return; hasResult = true; scannerController.stop().then((_) { widget.resultHandler(capture, resumeScanner); }); } @override void initState() { super.initState(); // Start listening to lifecycle changes. WidgetsBinding.instance.addObserver(this); // Start listening to the barcode events. subscription = scannerController.barcodes.listen(handleQRcode); // Finally, start the scanner itself. scannerController.start(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // If the controller is not ready, do not try to start or stop it. // Permission dialogs can trigger lifecycle changes before the controller is ready. if (!scannerController.value.isInitialized) return; switch (state) { case AppLifecycleState.detached: case AppLifecycleState.hidden: case AppLifecycleState.paused: return; case AppLifecycleState.resumed: // Restart the scanner when the app is resumed. // Don't forget to resume listening to the barcode events. subscription = scannerController.barcodes.listen(handleQRcode); scannerController.start(); break; case AppLifecycleState.inactive: // Stop the scanner when the app is paused. // Also stop the barcode events subscription. subscription?.cancel(); subscription = null; scannerController.stop(); break; } } @override Widget build(BuildContext context) { return MobileScanner( controller: scannerController, errorBuilder: (context, error, _) { final scheme = Theme.of(context).colorScheme; return ColoredBox( color: scheme.surface, child: Center( child: switch (error.errorCode) { MobileScannerErrorCode.controllerAlreadyInitialized || MobileScannerErrorCode.controllerDisposed || MobileScannerErrorCode.controllerUninitialized || MobileScannerErrorCode.genericError || MobileScannerErrorCode.unsupported => Icon(Icons.error, color: scheme.error), MobileScannerErrorCode.permissionDenied => Text( AppLocalizations.of(context)!.scanPagePermissionDeniedMsg, textAlign: TextAlign.center, ), }, ), ); }, ); } @override void dispose() async { // Stop listening to lifecycle changes. WidgetsBinding.instance.removeObserver(this); // Stop listening to the barcode events. subscription?.cancel(); subscription = null; super.dispose(); // Finally, dispose the controller. scannerController.dispose(); } }