Flutter二维码生成与扫描不一致问题技术求助
Flutter二维码生成与扫描可靠性问题排查与修复
问题描述
在Flutter应用中使用google_ml_kit读取图片扫描二维码,扫描功能基本正常,但存在以下问题:
- 生成的二维码有时有效可扫描,有时无效;
- 从相册选取图片扫描时,偶尔无法检测到二维码,扫描结果不稳定;
- 生成与扫描的不一致性导致扫描可靠性下降。
相关代码
生成页面代码
class QRCodeGeneratorScreen extends StatelessWidget { final qrCodeController = Get.put(QRCodeController()); QRCodeGeneratorScreen({super.key}); void _showQRCodeGeneratorDialog(BuildContext context) { final String data = qrCodeController.data.value; if (data.isNotEmpty) { showDialog( context: context, builder: (BuildContext context) { return QRCodeGeneratorDialog( title: 'Your Generated QR Code', data: data, ); }, ); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('No QR code data to display'), backgroundColor: Colors.black38, )); } } @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; return Scaffold( backgroundColor: Colors.white, appBar: const CustomAppBar( title: 'QR Code Generator', leftBottomRadius: 50, rightBottomRadius: 50, height: 100, showBackButton: true, centerTitle: false, backgroundColor: Colors.blue, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ TextFormField( decoration: const InputDecoration( labelText: 'Enter data for QR code', border: OutlineInputBorder(), ), onChanged: (value) { qrCodeController.updateData(value); }, ), const SizedBox(height: 20), Obx(() { final String data = Uri.encodeComponent(qrCodeController.data.value.trim()); return Center( child: data.isNotEmpty ? Column( mainAxisSize: MainAxisSize.min, children: [ QrImageView( data: data, version: QrVersions.auto, size: 200.0, backgroundColor: Colors.white, errorCorrectionLevel: QrErrorCorrectLevel.M, // Adjusted to Medium level ), SizedBox(height: size.height * 0.02), SizedBox( width: size.width * 0.5, child: ElevatedButton( onPressed: () => _showQRCodeGeneratorDialog(context), child: const Text( 'Show QR Code', style: TextStyle(color: Colors.white), ), ), ), ], ) : const Text('Enter data to generate QR code'), ); }), ], ), ), bottomNavigationBar: const CustomBottomNavigationBar(), ); } }
保存二维码弹窗代码
import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; import 'dart:io'; import '../history_screens/showCustomSnackBar.dart'; import 'package:path/path.dart' as path; class QRCodeGeneratorDialog extends StatefulWidget { final String title; final String data; const QRCodeGeneratorDialog( {super.key, required this.title, required this.data}); @override _QRCodeGeneratorDialogState createState() => _QRCodeGeneratorDialogState(); } class _QRCodeGeneratorDialogState extends State<QRCodeGeneratorDialog> { String? savedImagePath; Future<void> _launchURL() async { final Uri url = widget.data.startsWith('http') ? Uri.parse(widget.data) : Uri.parse('https://${widget.data}'); if (await canLaunchUrl(url)) { await launchUrl(url); } else { throw 'Could not launch $url'; } } Future<void> _saveImage() async { try { final qrValidationResult = QrValidator.validate( data: widget.data, version: QrVersions.auto, errorCorrectionLevel: QrErrorCorrectLevel.Q, ); if (qrValidationResult.status == QrValidationStatus.valid) { final qrPainter = QrPainter( data: widget.data, version: QrVersions.auto, gapless: true, errorCorrectionLevel: QrErrorCorrectLevel.Q, eyeStyle: const QrEyeStyle( eyeShape: QrEyeShape.square, color: Color(0xFF000000), ), dataModuleStyle: const QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: Color(0xFF000000), ), ); final pictureRecorder = PictureRecorder(); final canvas = Canvas(pictureRecorder); const double size = 600.0; // Ensure the QR code is rendered at a high resolution // Background color final paint = Paint()..color = Colors.white; canvas.drawRect(const Rect.fromLTWH(0, 0, size, size), paint); // Render the QR code qrPainter.paint(canvas, const Size(size, size)); // Convert the QR code to an image final image = await pictureRecorder.endRecording().toImage(size.toInt(), size.toInt()); final byteData = await image.toByteData(format: ImageByteFormat.png); final bytes = byteData!.buffer.asUint8List(); // Define the path to the internal storage/pictures/qrcode directory final directory = Directory('/storage/emulated/0/Pictures/QRCode'); if (!await directory.exists()) { await directory.create(recursive: true); // Create the directory if it doesn't exist } // Create a unique file name final imagePath = path.join(directory.path, 'qr_code_${DateTime.now().millisecondsSinceEpoch}.png'); final file = File(imagePath); await file.writeAsBytes(bytes); // Make the file visible to the gallery and other apps final result = await ImageGallerySaver.saveFile(file.path); Get.log('save path: ${file.path}'); if (result['isSuccess']) { showCustomSnackBar(Get.context!, 'Saved', 'Image saved to QRCode directory'); } else { showCustomSnackBar(Get.context!, 'Error', 'Error saving image'); } } else { showCustomSnackBar(Get.context!, 'Error', 'Invalid QR code data'); } } catch (e) { if (kDebugMode) { print(e); } showCustomSnackBar(Get.context!, 'Error', 'Failed to capture image'); } } Widget _buildIconButton( BuildContext context, VoidCallback onPressed, IconData icon) { return Container( decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(10), ), child: IconButton( onPressed: onPressed, icon: Icon(icon), ), ); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), ), ), child: Text( widget.title, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), Padding( padding: const EdgeInsets.all(16.0), child: QrImageView( data: widget.data, version: QrVersions.auto, size: 200.0, backgroundColor: Colors.white, ), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildIconButton( context, _launchURL, Icons.chrome_reader_mode_outlined), SizedBox(width: size.width * 0.02), _buildIconButton( context, () { Clipboard.setData(ClipboardData(text: widget.data)); showCustomSnackBar( context, 'Copied', 'Text copied to clipboard'); }, Icons.copy, ), SizedBox(width: size.width * 0.02), _buildIconButton(context, _saveImage, Icons.save), ], ), const SizedBox(height: 16), ], ), ); } }
控制器代码
class QRCodeController extends GetxController { final GlobalKey qrKey = GlobalKey(debugLabel: 'GenScreenQR'); var data = ''.obs; // Observable for QR code data void updateData(String newData) { data.value = newData; } }
图片选择与处理代码
Future<void> pickImage() async { try { final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { final imagePath = pickedFile.path; Get.log('Selected image path: $imagePath'); final file = File(imagePath); if (await file.exists()) { selectedImage.value = file; } else { Get.log('File does not exist at the path: $imagePath'); } } else { Get.log('No image selected.'); } } catch (e) { Get.log('Error picking or processing image: $e'); } } Future<void> processImageForQRCode(String imagePath) async { try { final file = File(imagePath); if (!await file.exists()) { throw 'File does not exist at the specified path.'; } Get.log('Processing image at path: $imagePath'); final inputImage = InputImage.fromFilePath(imagePath); // Log the image metadata if possible final imageInfo = await _getImageInfo(imagePath); Get.log('Image info: $imageInfo'); const int maxRetries = 3; int attempts = 0; List<Barcode> barcodes = []; while (barcodes.isEmpty && attempts < maxRetries) { attempts++; barcodes = await barcodeScanner.processImage(inputImage); Get.log( 'Attempt $attempts: Number of barcodes detected: ${barcodes.length}'); if (barcodes.isEmpty && attempts < maxRetries) { await Future.delayed(const Duration(milliseconds: 500)); } } if (barcodes.isEmpty) { Get.log('No QR Code detected.'); Get.snackbar( 'No QR Code Detected', 'No QR code was detected in the image.', snackPosition: SnackPosition.BOTTOM, ); } else { final Set<String> processedCodes = {}; for (Barcode barcode in barcodes) { final String? qrCodeValue = barcode.rawValue; Get.log('Found QR Code value: $qrCodeValue'); if (qrCodeValue != null && !processedCodes.contains(qrCodeValue)) { processedCodes.add(qrCodeValue); if (!scanHistory.contains(qrCodeValue)) { scanHistory.add(qrCodeValue); SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setStringList('scanHistory', scanHistory.toList()); Get.log('QR Code Result: $qrCodeValue'); Get.snackbar( 'QR Code Scanned', qrCodeValue, snackPosition: SnackPosition.BOTTOM, ); await Future.delayed(const Duration(milliseconds: 300)); Get.offAll(() => HistoryScreen()); } } } } } catch (e) { Get.log('Error processing the image: $e'); Get.snackbar( 'Error', 'An error occurred while processing the image.', snackPosition: SnackPosition.BOTTOM, ); } } Future<Map<String, dynamic>> _getImageInfo(String imagePath) async { // Implement a method to retrieve and return image metadata if necessary return {'size': await File(imagePath).length()}; }
解决方案
1. 统一二维码生成的数据处理
生成页面中使用了Uri.encodeComponent对数据进行编码,但弹窗和保存逻辑使用的是原始未编码数据,导致生成的二维码内容不一致,这是生成与扫描不一致的核心原因。移除编码处理,确保所有生成二维码的环节使用相同的原始数据:
// 修改生成页面的Obx部分 Obx(() { final String data = qrCodeController.data.value.trim(); return Center( child: data.isNotEmpty ? Column( mainAxisSize: MainAxisSize.min, children: [ QrImageView( data: data, version: QrVersions.auto, size: 200.0, backgroundColor: Colors.white, errorCorrectionLevel: QrErrorCorrectLevel.Q, // 和弹窗统一纠错级别 ), // 后续代码不变 ], ) : const Text('Enter data to generate QR code'), ); })
2. 统一二维码纠错级别
将生成页面、弹窗中的纠错级别统一设置为QrErrorCorrectLevel.Q或H,提升二维码的容错能力,即使二维码有轻微模糊、污渍也能被识别。
3. 优化扫描逻辑
- 指定扫描类型:创建
BarcodeScanner时指定只扫描QR码,避免其他条码类型干扰:final barcodeScanner = BarcodeScanner(barcodeFormat: BarcodeFormat.qrCode); - 移除无效重试逻辑:
processImage是同步处理,重试不会改变识别结果,可移除循环重试代码; - 优化图片读取方式:使用
InputImage.fromFile替代fromFilePath,确保文件读取的准确性:final inputImage = InputImage.fromFile(file); - 图片预处理:如果图片模糊或对比度低,可使用
image库调整图片的亮度、对比度,或缩放图片让二维码占比更合适,提升识别率。
4. 确保保存的二维码清晰度
保存二维码时保持600x600的高分辨率,且使用PNG无损格式,避免压缩导致的二维码模块模糊,影响扫描识别。
内容的提问来源于stack exchange,提问作者afra janjua
相关产品推荐
相关产品推荐

