You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 18:37:31