Flutter: How to pause and restart a `Stream` listener?

Viewed 2468

I am developing a flutter app to read QR Codes. I am using qr_code_scanner: ^0.3.5 library. Below is my code.

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';

class ScanQRCodeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(
          title: Text("Scan QR Code"),
        ),
        body: _ScanQRCodeUI());
  }
}

class _ScanQRCodeUI extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ScanQRCodeUIState();
  }
}

class _ScanQRCodeUIState extends State<_ScanQRCodeUI> {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  Barcode result;
  QRViewController controller;

  // In order to get hot reload to work we need to pause the camera if the platform
  // is android, or resume the camera if the platform is iOS.
  @override
  void reassemble() {
    super.reassemble();
    if (Platform.isAndroid) {
      controller.pauseCamera();
    } else if (Platform.isIOS) {
      controller.resumeCamera();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(flex: 4, child: _buildQrView(context)),
        Expanded(flex: 1, child: _dataDisplayUI())
      ],
    );
  }

  Widget _buildQrView(BuildContext context) {
    // For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
    var scanArea = (MediaQuery.of(context).size.width < 400 ||
            MediaQuery.of(context).size.height < 400)
        ? 200.0
        : 400.0;
    // To ensure the Scanner view is properly sizes after rotation
    // we need to listen for Flutter SizeChanged notification and update controller
    return QRView(
      key: qrKey,
      onQRViewCreated: _onQRViewCreated,
      overlay: QrScannerOverlayShape(
          borderColor: Colors.red,
          borderRadius: 10,
          borderLength: 30,
          borderWidth: 10,
          cutOutSize: scanArea),
    );
  }

  void _onQRViewCreated(QRViewController controller) {
    setState(() {
      this.controller = controller;
    });

    controller.scannedDataStream.listen((scanData) async {
      print("Hello0");
      setState(() {
        result = scanData;
        print(result.code);
      });

      // await controller.pauseCamera();
    });
  }

  Widget _dataDisplayUI() {
    const yellowColor = const Color(0xffEDE132);

    return Column(
      children: [
        Row(
          children: [
            Expanded(
                flex: 7,
                child: Container(
                  margin:
                      EdgeInsets.only(top: 30, bottom: 30, left: 10, right: 10),
                  child:
                      Text("You have added 12 products. Click here to publish.",
                          style: GoogleFonts.poppins(
                              textStyle: TextStyle(
                            color: Colors.black,
                            fontWeight: FontWeight.normal,
                            fontSize: 14,
                          ))),
                )),
            Expanded(
                flex: 3,
                child: Container(
                  width: 60,
                  height: 60,
                  child: Center(
                      child: Text("12",
                          style: GoogleFonts.poppins(
                              textStyle: TextStyle(
                            color: Colors.black,
                            fontWeight: FontWeight.bold,
                            fontSize: 21,
                          )))),
                  decoration:
                      BoxDecoration(shape: BoxShape.circle, color: yellowColor),
                ))
          ],
        )
      ],
    );
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}

I am using this app to scan products one by one, just like how the cashier does in a super marker using a barcode scanner.

The issue is, this scanner is listening to a stream and it is keep on running. Pay your attention to the _onQRViewCreated method. As a result, the same QR is being read multiple times before we even move the camera to the next QR code.

How can I make sure there is a delay between 2 scans? For an example, when I scan for a QR Code, I have to wait for another 2 seconds to scan the next QR.

If my idea of creating a delay between 2 scans is wrong, I am open for other ideas as well.

2 Answers

You can use

controller.scannedDataStream.first

that stops listening to other events from the stream.

Another solution would be to set an internal state property like below:

bool QrBeingProcessed = false;

and when you scan the first qr set it to true, till you're done.

Another way would be to use DateTime to check and see if the time difference between the current scan and the last scan is under a specified amount (say 3 seconds). If so, we don't set the state to the new scanData. In effect, this puts a 3-second delay between each QR code scan. See the code below.

     controller.scannedDataStream.listen((scanData) {
      final currentScan = DateTime.now();
      if (lastScan == null || currentScan.difference(lastScan!) > const Duration(seconds: 3)) 
      {
        lastScan = currentScan;
        print(scanData.code);
  
        setState(() {
          result = scanData;
          print('the qr just read was ' + scanData.code);
        });
     }
  });

}

Related