Skip to content

Continuous Scan - previous listeners still active on subsequent calls. #111

Open
@murphkev

Description

@murphkev

Flutter 3.29.0
Dart 3.7.0

Continuous scan will work fine the first time. However, after closing and reopening the previous listeners will still be active; even if the subscription has been cancelled, and the stream closed (from streamBarcode function).
The result is repeated errors relating to adding an event to a closed stream; one for each instance of opening the continuous scan.

Unhandled Exception: Bad state: Cannot add event after closing stream

It's easy to recreate. I'm pretty sure the example code would get the same result.

I think the error originates in the BarcodeScanner class in io_device.dart. The subscriptions don't seem to be effectively cancelled.
I've copied the class and managed the subscriptions for the broadcast stream so it can close correctly, and it seems to be working as expected now.

Additionally isPopped in streamBarcode might be unnecessary? I changed it to context.mounted on my journey to finding the above issue.

Here's the copy of BarcodeScanner that I made. I'm not sure if this code is robust enough, and I don't know the internal working very well. Hopefully it can help you resolve the issue?

Changes are pretty minor:

  • change to StatefulWidget
  • add functions to manage subscriptions
  • remove listeners onDispose
  • modify _streamBarcodeForMobileAndTabDevices to work with above
/// Barcode scanner for mobile and desktop devices
class BarcodeScannerCopy extends StatefulWidget {
  final String lineColor;
  final String cancelButtonText;
  final bool isShowFlashIcon;
  final ScanType scanType;
  final CameraFace cameraFace;
  final Function(String) onScanned;
  final String? appBarTitle;
  final bool? centerTitle;
  final Widget? child;
  final BarcodeAppBar? barcodeAppBar;
  final int? delayMillis;
  final Function? onClose;
  final ScanFormat scanFormat;

  const BarcodeScannerCopy({
    super.key,
    required this.lineColor,
    required this.cancelButtonText,
    required this.isShowFlashIcon,
    required this.scanType,
    this.cameraFace = CameraFace.back,
    required this.onScanned,
    this.child,
    this.appBarTitle,
    this.centerTitle,
    this.barcodeAppBar,
    this.delayMillis,
    this.onClose,
    this.scanFormat = ScanFormat.ALL_FORMATS
  });

  @override
  State<BarcodeScannerCopy> createState() => BarcodeScannerCopyState();
}

class BarcodeScannerCopyState extends State<BarcodeScannerCopy> {

  @override
  void dispose() {
    removeAllListeners();
    super.dispose();
  }

  List<StreamSubscription> subscriptions = [];
  Stream? stream;

  void addListener() {
    StreamSubscription? subscription = stream?.listen(onData);
    if (subscription != null) {
      subscriptions.add(subscription);
    }
  }

  void removeAllListeners() {
    print('removing all listeners...');
    for (StreamSubscription subscription in subscriptions) {
      subscription.cancel();
    }
    subscriptions.clear();
  }

  void onData(barcode) {
    print("\n\n\nrecevied a barcode");
    barcode == kCancelValue ? widget.onClose?.call() : widget.onScanned(barcode);
  }

  @override
  Widget build(BuildContext context) {
    if (Platform.isWindows) {
      ///Get Window barcode Scanner UI
      return WindowBarcodeScanner(
        lineColor: widget.lineColor,
        cancelButtonText: widget.cancelButtonText,
        isShowFlashIcon: widget.isShowFlashIcon,
        scanType: widget.scanType,
        onScanned: widget.onScanned,
        appBarTitle: widget.appBarTitle,
        centerTitle: widget.centerTitle,
        delayMillis: widget.delayMillis,
      );
    } else {
      /// Scan Android and ios barcode scanner with flutter_barcode_scanner
      /// If onClose is not null then stream barcode otherwise scan barcode
      /// Scan barcode for mobile devices
      ScanMode scanMode;
      switch (widget.scanType) {
        case ScanType.barcode:
          scanMode = ScanMode.BARCODE;
          break;
        case ScanType.qr:
          scanMode = ScanMode.QR;
          break;
        default:
          scanMode = ScanMode.DEFAULT;
          break;
      }
      widget.onClose != null
          ? _streamBarcodeForMobileAndTabDevices(scanMode)
          : _scanBarcodeForMobileAndTabDevices(scanMode);

      return const Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    }
  }

  _scanBarcodeForMobileAndTabDevices(ScanMode scanMode) async {
    String barcode = await FlutterBarcodeScanner.scanBarcode(
      widget.lineColor,
      widget.cancelButtonText,
      widget.isShowFlashIcon,
      scanMode,
      widget.delayMillis,
      widget.cameraFace.name.toUpperCase(),
      widget.scanFormat,
    );
    widget.onScanned(barcode);
  }

  void _streamBarcodeForMobileAndTabDevices(ScanMode scanMode) {
    stream = FlutterBarcodeScanner.getBarcodeStreamReceiver(
      widget.lineColor,
      widget.cancelButtonText,
      widget.isShowFlashIcon,
      scanMode,
      widget.delayMillis,
      widget.cameraFace.name.toUpperCase(),
      widget.scanFormat,
    );
    addListener();
  }
}

// This is for scanner Widget, which is used to scan the barcode.

typedef BarcodeScannerViewCreated = void Function(
    BarcodeViewController controller);

/// for widgets
class BarcodeScannerView extends StatelessWidget {
  final BarcodeScannerViewCreated onBarcodeViewCreated;
  final double? scannerWidth;
  final double? scannerHeight;
  final ScanType scanType;
  final CameraFace cameraFace;
  final Function(String)? onScanned;
  final Widget? child;
  final int? delayMillis;
  final Function? onClose;
  final bool continuous;
  final ScanFormat scanFormat;
  const BarcodeScannerView(
      {super.key,
      this.scannerWidth,
      this.scannerHeight,
      required this.scanType,
      this.cameraFace = CameraFace.back,
      required this.onScanned,
      this.continuous = false,
      this.child,
      this.delayMillis,
      this.onClose,
      this.scanFormat = ScanFormat.ALL_FORMATS,
      required this.onBarcodeViewCreated});

  @override
  Widget build(BuildContext context) {
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return AndroidView(
          viewType: 'plugins.codingwithtashi/barcode_scanner_view',
          onPlatformViewCreated: _onPlatformViewCreated,
          creationParams: <String, dynamic>{
            'scanType': scanType.index,
            'cameraFace': cameraFace.index,
            'delayMillis': delayMillis,
            'continuous': continuous,
            'scannerWidth': scannerWidth?.toInt(),
            'scannerHeight': scannerHeight?.toInt(),
            'scanFormat': scanFormat.name,
          },
          creationParamsCodec: const StandardMessageCodec(),
        );
      case TargetPlatform.iOS:
        return UiKitView(
          viewType: 'plugins.codingwithtashi/barcode_scanner_view',
          onPlatformViewCreated: _onPlatformViewCreated,
          creationParams: <String, dynamic>{
            'scanType': scanType.index,
            'cameraFace': cameraFace.index,
            'delayMillis': delayMillis,
            'continuous': continuous,
            'scannerWidth': scannerWidth?.toInt(),
            'scannerHeight': scannerHeight?.toInt(),
            'scanFormat': scanFormat.name,
          },
          creationParamsCodec: const StandardMessageCodec(),
        );
      default:
        return Text(
            '$defaultTargetPlatform is not yet supported by the web_view plugin');
    }
  }

  // Callback method when platform view is created

  void _onPlatformViewCreated(int id) {
    final controller = BarcodeViewController.data(id);
    if (onScanned != null) {
      controller.setOnScanned(onScanned!);
    }
    onBarcodeViewCreated(controller);
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions