Description
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);
}
}