Navaron Bracke

reimplement parts of the MobileScannerController using the platform interface

1 import 'dart:async'; 1 import 'dart:async';
2 -import 'dart:io';  
3 -// ignore: unnecessary_import  
4 -import 'dart:typed_data';  
5 2
6 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
7 import 'package:flutter/services.dart'; 4 import 'package:flutter/services.dart';
@@ -12,53 +9,45 @@ import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; @@ -12,53 +9,45 @@ import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
12 import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart'; 9 import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart';
13 import 'package:mobile_scanner/src/enums/torch_state.dart'; 10 import 'package:mobile_scanner/src/enums/torch_state.dart';
14 import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; 11 import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
15 -import 'package:mobile_scanner/src/objects/barcode.dart'; 12 +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
16 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 13 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
17 -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';  
18 14
19 -/// The [MobileScannerController] holds all the logic of this plugin,  
20 -/// where as the [MobileScanner] class is the frontend of this plugin.  
21 -class MobileScannerController { 15 +/// The controller for the [MobileScanner] widget.
  16 +class MobileScannerController extends ValueNotifier<MobileScannerState> {
  17 + /// Construct a new [MobileScannerController] instance.
22 MobileScannerController({ 18 MobileScannerController({
23 - this.facing = CameraFacing.back, 19 + this.cameraResolution,
24 this.detectionSpeed = DetectionSpeed.normal, 20 this.detectionSpeed = DetectionSpeed.normal,
25 - this.detectionTimeoutMs = 250,  
26 - this.torchEnabled = false,  
27 - this.formats, 21 + int detectionTimeoutMs = 250,
  22 + this.facing = CameraFacing.back,
  23 + this.formats = const <BarcodeFormat>[],
28 this.returnImage = false, 24 this.returnImage = false,
29 - @Deprecated(  
30 - 'Instead, use the result of calling `start()` to determine if permissions were granted.',  
31 - )  
32 - this.onPermissionSet,  
33 - this.autoStart = true,  
34 - this.cameraResolution, 25 + this.torchEnabled = false,
35 this.useNewCameraSelector = false, 26 this.useNewCameraSelector = false,
36 - }); 27 + }) : detectionTimeoutMs = detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0,
  28 + assert(detectionTimeoutMs >= 0, 'The detection timeout must be greater than or equal to 0.'),
  29 + super(MobileScannerState.uninitialized(facing));
37 30
38 - /// Select which camera should be used. 31 + /// The desired resolution for the camera.
39 /// 32 ///
40 - /// Default: CameraFacing.back  
41 - final CameraFacing facing;  
42 -  
43 - /// Enable or disable the torch (Flash) on start 33 + /// When this value is provided, the camera will try to match this resolution,
  34 + /// or fallback to the closest available resolution.
  35 + /// When this is null, Android defaults to a resolution of 640x480.
44 /// 36 ///
45 - /// Default: disabled  
46 - final bool torchEnabled;  
47 -  
48 - /// Set to true if you want to return the image buffer with the Barcode event 37 + /// Bear in mind that changing the resolution has an effect on the aspect ratio.
49 /// 38 ///
50 - /// Only supported on iOS and Android  
51 - final bool returnImage;  
52 -  
53 - /// If provided, the scanner will only detect those specific formats  
54 - final List<BarcodeFormat>? formats; 39 + /// When the camera orientation changes,
  40 + /// the resolution will be flipped to match the new dimensions of the display.
  41 + ///
  42 + /// Currently only supported on Android.
  43 + final Size? cameraResolution;
55 44
56 - /// Sets the speed of detections. 45 + /// The detection speed for the scanner.
57 /// 46 ///
58 - /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices 47 + /// Defaults to [DetectionSpeed.normal].
59 final DetectionSpeed detectionSpeed; 48 final DetectionSpeed detectionSpeed;
60 49
61 - /// Sets the timeout, in milliseconds, of the scanner. 50 + /// The detection timeout, in milliseconds, for the scanner.
62 /// 51 ///
63 /// This timeout is ignored if the [detectionSpeed] 52 /// This timeout is ignored if the [detectionSpeed]
64 /// is not set to [DetectionSpeed.normal]. 53 /// is not set to [DetectionSpeed.normal].
@@ -67,444 +56,123 @@ class MobileScannerController { @@ -67,444 +56,123 @@ class MobileScannerController {
67 /// which prevents memory issues on older devices. 56 /// which prevents memory issues on older devices.
68 final int detectionTimeoutMs; 57 final int detectionTimeoutMs;
69 58
70 - /// Automatically start the mobileScanner on initialization.  
71 - final bool autoStart; 59 + /// The facing direction for the camera.
  60 + ///
  61 + /// Defaults to the back-facing camera.
  62 + final CameraFacing facing;
72 63
73 - /// The desired resolution for the camera. 64 + /// The formats that the scanner should detect.
74 /// 65 ///
75 - /// When this value is provided, the camera will try to match this resolution,  
76 - /// or fallback to the closest available resolution.  
77 - /// When this is null, Android defaults to a resolution of 640x480. 66 + /// If this is empty, all supported formats are detected.
  67 + final List<BarcodeFormat> formats;
  68 +
  69 + /// Whether scanned barcodes should contain the image
  70 + /// that is embedded into the barcode.
78 /// 71 ///
79 - /// Bear in mind that changing the resolution has an effect on the aspect ratio. 72 + /// If this is false, [BarcodeCapture.image] will always be null.
80 /// 73 ///
81 - /// When the camera orientation changes,  
82 - /// the resolution will be flipped to match the new dimensions of the display. 74 + /// Defaults to false, and is only supported on iOS and Android.
  75 + final bool returnImage;
  76 +
  77 + /// Whether the flashlight should be turned on when the camera is started.
83 /// 78 ///
84 - /// Currently only supported on Android.  
85 - final Size? cameraResolution; 79 + /// Defaults to false.
  80 + final bool torchEnabled;
86 81
87 - /// Use the new resolution selector. Warning: not fully tested, may produce  
88 - /// unwanted/zoomed images. 82 + /// Use the new resolution selector.
  83 + ///
  84 + /// This feature is experimental and not fully tested yet.
  85 + /// Use caution when using this flag,
  86 + /// as the new resolution selector may produce unwanted or zoomed images.
89 /// 87 ///
90 - /// Only supported on Android 88 + /// Only supported on Android.
91 final bool useNewCameraSelector; 89 final bool useNewCameraSelector;
92 90
93 - /// Sets the barcode stream  
94 - final StreamController<BarcodeCapture> _barcodesController =  
95 - StreamController.broadcast(); 91 + /// The internal barcode controller, that listens for detected barcodes.
  92 + final StreamController<BarcodeCapture> _barcodesController = StreamController.broadcast();
96 93
  94 + /// Get the stream of scanned barcodes.
97 Stream<BarcodeCapture> get barcodes => _barcodesController.stream; 95 Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
98 96
99 - static const MethodChannel _methodChannel =  
100 - MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');  
101 - static const EventChannel _eventChannel =  
102 - EventChannel('dev.steenbakker.mobile_scanner/scanner/event');  
103 -  
104 - @Deprecated(  
105 - 'Instead, use the result of calling `start()` to determine if permissions were granted.',  
106 - )  
107 - Function(bool permissionGranted)? onPermissionSet;  
108 -  
109 - /// Listen to events from the platform specific code  
110 - StreamSubscription? events;  
111 -  
112 - /// A notifier that provides several arguments about the MobileScanner  
113 - final ValueNotifier<MobileScannerArguments?> startArguments =  
114 - ValueNotifier(null);  
115 -  
116 - /// A notifier that provides the state of the Torch (Flash)  
117 - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);  
118 -  
119 - /// A notifier that provides the state of which camera is being used  
120 - late final ValueNotifier<CameraFacing> cameraFacingState =  
121 - ValueNotifier(facing);  
122 -  
123 - /// A notifier that provides zoomScale.  
124 - final ValueNotifier<double> zoomScaleState = ValueNotifier(0.0);  
125 -  
126 - bool isStarting = false;  
127 -  
128 - /// A notifier that provides availability of the Torch (Flash)  
129 - final ValueNotifier<bool?> hasTorchState = ValueNotifier(false);  
130 -  
131 - /// Returns whether the device has a torch. 97 + /// Analyze an image file.
132 /// 98 ///
133 - /// Throws an error if the controller is not initialized.  
134 - bool get hasTorch {  
135 - final hasTorch = hasTorchState.value;  
136 - if (hasTorch == null) {  
137 - throw const MobileScannerException(  
138 - errorCode: MobileScannerErrorCode.controllerUninitialized,  
139 - );  
140 - }  
141 -  
142 - return hasTorch; 99 + /// The [path] points to a file on the device.
  100 + ///
  101 + /// This is only supported on Android and iOS.
  102 + ///
  103 + /// Returns the [BarcodeCapture] that was found in the image.
  104 + Future<BarcodeCapture?> analyzeImage(String path) {
  105 + return MobileScannerPlatform.instance.analyzeImage(path);
143 } 106 }
144 107
145 - /// Set the starting arguments for the camera  
146 - Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {  
147 - final Map<String, dynamic> arguments = {};  
148 -  
149 - cameraFacingState.value = cameraFacingOverride ?? facing;  
150 - arguments['facing'] = cameraFacingState.value.rawValue;  
151 - arguments['torch'] = torchEnabled;  
152 - arguments['speed'] = detectionSpeed.rawValue;  
153 - arguments['timeout'] = detectionTimeoutMs;  
154 - arguments['returnImage'] = returnImage;  
155 - arguments['useNewCameraSelector'] = useNewCameraSelector;  
156 -  
157 - /* if (scanWindow != null) {  
158 - arguments['scanWindow'] = [  
159 - scanWindow!.left,  
160 - scanWindow!.top,  
161 - scanWindow!.right,  
162 - scanWindow!.bottom,  
163 - ];  
164 - } */  
165 -  
166 - if (formats != null) {  
167 - if (kIsWeb || Platform.isIOS || Platform.isMacOS || Platform.isAndroid) {  
168 - arguments['formats'] = formats!.map((e) => e.rawValue).toList();  
169 - }  
170 - }  
171 -  
172 - if (cameraResolution != null) {  
173 - arguments['cameraResolution'] = <int>[  
174 - cameraResolution!.width.toInt(),  
175 - cameraResolution!.height.toInt(),  
176 - ];  
177 - }  
178 -  
179 - return arguments; 108 + /// Reset the zoom scale of the camera.
  109 + Future<void> resetZoomScale() async {
  110 + await MobileScannerPlatform.instance.resetZoomScale();
180 } 111 }
181 112
182 - /// Start scanning for barcodes.  
183 - /// Upon calling this method, the necessary camera permission will be requested. 113 + /// Set the zoom scale of the camera.
184 /// 114 ///
185 - /// Returns an instance of [MobileScannerArguments]  
186 - /// when the scanner was successfully started.  
187 - /// Returns null if the scanner is currently starting.  
188 - ///  
189 - /// Throws a [MobileScannerException] if starting the scanner failed.  
190 - Future<MobileScannerArguments?> start({  
191 - CameraFacing? cameraFacingOverride,  
192 - }) async {  
193 - if (isStarting) {  
194 - debugPrint("Called start() while starting.");  
195 - return null;  
196 - }  
197 -  
198 - events ??= _eventChannel  
199 - .receiveBroadcastStream()  
200 - .listen((data) => _handleEvent(data as Map));  
201 -  
202 - isStarting = true;  
203 -  
204 - // Check authorization status  
205 - if (!kIsWeb) {  
206 - final MobileScannerState state;  
207 -  
208 - try {  
209 - state = MobileScannerState.fromRawValue(  
210 - await _methodChannel.invokeMethod('state') as int? ?? 0,  
211 - );  
212 - } on PlatformException catch (error) {  
213 - isStarting = false;  
214 -  
215 - throw MobileScannerException(  
216 - errorCode: MobileScannerErrorCode.genericError,  
217 - errorDetails: MobileScannerErrorDetails(  
218 - code: error.code,  
219 - details: error.details as Object?,  
220 - message: error.message,  
221 - ),  
222 - );  
223 - }  
224 -  
225 - switch (state) {  
226 - // Android does not have an undetermined permission state.  
227 - // So if the permission state is denied, just request it now.  
228 - case MobileScannerState.undetermined:  
229 - case MobileScannerState.denied:  
230 - try {  
231 - final bool granted =  
232 - await _methodChannel.invokeMethod('request') as bool? ?? false;  
233 -  
234 - if (!granted) {  
235 - isStarting = false;  
236 - throw const MobileScannerException(  
237 - errorCode: MobileScannerErrorCode.permissionDenied,  
238 - );  
239 - }  
240 - } on PlatformException catch (error) {  
241 - isStarting = false;  
242 - throw MobileScannerException(  
243 - errorCode: MobileScannerErrorCode.genericError,  
244 - errorDetails: MobileScannerErrorDetails(  
245 - code: error.code,  
246 - details: error.details as Object?,  
247 - message: error.message,  
248 - ),  
249 - );  
250 - }  
251 -  
252 - case MobileScannerState.authorized:  
253 - break;  
254 - }  
255 - }  
256 -  
257 - // Start the camera with arguments  
258 - Map<String, dynamic>? startResult = {};  
259 - try {  
260 - startResult = await _methodChannel.invokeMapMethod<String, dynamic>(  
261 - 'start',  
262 - _argumentsToMap(cameraFacingOverride: cameraFacingOverride),  
263 - );  
264 - } on PlatformException catch (error) {  
265 - MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;  
266 -  
267 - final String? errorMessage = error.message;  
268 -  
269 - if (kIsWeb) {  
270 - if (errorMessage == null) {  
271 - errorCode = MobileScannerErrorCode.genericError;  
272 - } else if (errorMessage.contains('NotFoundError') ||  
273 - errorMessage.contains('NotSupportedError')) {  
274 - errorCode = MobileScannerErrorCode.unsupported;  
275 - } else if (errorMessage.contains('NotAllowedError')) {  
276 - errorCode = MobileScannerErrorCode.permissionDenied;  
277 - } else {  
278 - errorCode = MobileScannerErrorCode.genericError;  
279 - }  
280 - }  
281 -  
282 - isStarting = false;  
283 -  
284 - throw MobileScannerException(  
285 - errorCode: errorCode,  
286 - errorDetails: MobileScannerErrorDetails(  
287 - code: error.code,  
288 - details: error.details as Object?,  
289 - message: error.message,  
290 - ),  
291 - );  
292 - }  
293 -  
294 - if (startResult == null) {  
295 - isStarting = false; 115 + /// The [zoomScale] must be between 0.0 and 1.0 (both inclusive).
  116 + Future<void> setZoomScale(double zoomScale) async {
  117 + if (zoomScale < 0 || zoomScale > 1) {
296 throw const MobileScannerException( 118 throw const MobileScannerException(
297 errorCode: MobileScannerErrorCode.genericError, 119 errorCode: MobileScannerErrorCode.genericError,
  120 + errorDetails: MobileScannerErrorDetails(
  121 + message: 'The zoomScale must be between 0.0 and 1.0',
  122 + ),
298 ); 123 );
299 } 124 }
300 125
301 - final hasTorch = startResult['torchable'] as bool? ?? false;  
302 - hasTorchState.value = hasTorch;  
303 -  
304 - final Size size;  
305 -  
306 - if (kIsWeb) {  
307 - size = Size(  
308 - startResult['videoWidth'] as double? ?? 0,  
309 - startResult['videoHeight'] as double? ?? 0,  
310 - );  
311 - } else {  
312 - final Map<Object?, Object?>? sizeInfo =  
313 - startResult['size'] as Map<Object?, Object?>?;  
314 -  
315 - size = Size(  
316 - sizeInfo?['width'] as double? ?? 0,  
317 - sizeInfo?['height'] as double? ?? 0,  
318 - );  
319 - }  
320 -  
321 - isStarting = false;  
322 - return startArguments.value = MobileScannerArguments(  
323 - numberOfCameras: startResult['numberOfCameras'] as int?,  
324 - size: size,  
325 - hasTorch: hasTorch,  
326 - textureId: kIsWeb ? null : startResult['textureId'] as int?,  
327 - webId: kIsWeb ? startResult['ViewID'] as String? : null,  
328 - ); 126 + await MobileScannerPlatform.instance.setZoomScale(zoomScale);
329 } 127 }
330 128
331 - /// Stops the camera, but does not dispose this controller. 129 + /// Stop the camera.
  130 + ///
  131 + /// After calling this method, the camera can be restarted using [start].
332 Future<void> stop() async { 132 Future<void> stop() async {
333 - await _methodChannel.invokeMethod('stop'); 133 + await MobileScannerPlatform.instance.stop();
334 134
335 // After the camera stopped, set the torch state to off, 135 // After the camera stopped, set the torch state to off,
336 // as the torch state callback is never called when the camera is stopped. 136 // as the torch state callback is never called when the camera is stopped.
337 torchState.value = TorchState.off; 137 torchState.value = TorchState.off;
338 } 138 }
339 139
340 - /// Switches the torch on or off.  
341 - ///  
342 - /// Does nothing if the device has no torch.  
343 - ///  
344 - /// Throws if the controller was not initialized.  
345 - Future<void> toggleTorch() async {  
346 - final hasTorch = hasTorchState.value;  
347 -  
348 - if (hasTorch == null) {  
349 - throw const MobileScannerException(  
350 - errorCode: MobileScannerErrorCode.controllerUninitialized,  
351 - );  
352 - } 140 + /// Switch between the front and back camera.
  141 + Future<void> switchCamera() async {
  142 + await MobileScannerPlatform.instance.stop();
353 143
354 - if (!hasTorch) {  
355 - return;  
356 - } 144 + final CameraFacing cameraDirection;
357 145
358 - final TorchState newState =  
359 - torchState.value == TorchState.off ? TorchState.on : TorchState.off; 146 + // TODO: update the camera facing direction state
360 147
361 - await _methodChannel.invokeMethod('torch', newState.rawValue); 148 + await start(cameraDirection: cameraDirection);
362 } 149 }
363 150
364 - /// Changes the state of the camera (front or back). 151 + /// Switches the flashlight on or off.
365 /// 152 ///
366 - /// Does nothing if the device has no front camera.  
367 - Future<void> switchCamera() async {  
368 - await _methodChannel.invokeMethod('stop');  
369 - final CameraFacing facingToUse =  
370 - cameraFacingState.value == CameraFacing.back  
371 - ? CameraFacing.front  
372 - : CameraFacing.back;  
373 - await start(cameraFacingOverride: facingToUse);  
374 - }  
375 -  
376 - /// Handles a local image file.  
377 - /// Returns true if a barcode or QR code is found.  
378 - /// Returns false if nothing is found. 153 + /// Does nothing if the device has no torch.
379 /// 154 ///
380 - /// [path] The path of the image on the devices  
381 - Future<bool> analyzeImage(String path) async {  
382 - events ??= _eventChannel  
383 - .receiveBroadcastStream()  
384 - .listen((data) => _handleEvent(data as Map));  
385 -  
386 - return _methodChannel  
387 - .invokeMethod<bool>('analyzeImage', path)  
388 - .then<bool>((bool? value) => value ?? false);  
389 - } 155 + /// Throws if the controller was not initialized.
  156 + Future<void> toggleTorch() async {
  157 + final bool hasTorch;
390 158
391 - /// Set the zoomScale of the camera.  
392 - ///  
393 - /// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0  
394 - /// is zoomed out.  
395 - Future<void> setZoomScale(double zoomScale) async {  
396 - if (zoomScale < 0 || zoomScale > 1) {  
397 - throw const MobileScannerException(  
398 - errorCode: MobileScannerErrorCode.genericError,  
399 - errorDetails: MobileScannerErrorDetails(  
400 - message: 'The zoomScale must be between 0 and 1.',  
401 - ),  
402 - ); 159 + if (!hasTorch) {
  160 + return;
403 } 161 }
404 - await _methodChannel.invokeMethod('setScale', zoomScale);  
405 - }  
406 162
407 - /// Reset the zoomScale of the camera to use standard scale 1x.  
408 - Future<void> resetZoomScale() async {  
409 - await _methodChannel.invokeMethod('resetScale');  
410 - } 163 + final TorchState newState = torchState.value == TorchState.off ? TorchState.on : TorchState.off;
411 164
412 - /// Disposes the MobileScannerController and closes all listeners.  
413 - ///  
414 - /// If you call this, you cannot use this controller object anymore.  
415 - void dispose() {  
416 - stop();  
417 - events?.cancel();  
418 - _barcodesController.close(); 165 + // Update the torch state to the new state.
  166 + // When the platform has updated the torch state,
  167 + // it will send an update through the torch state event stream.
  168 + await MobileScannerPlatform.instance.setTorchState();
419 } 169 }
420 170
421 - /// Handles a returning event from the platform side  
422 - void _handleEvent(Map event) {  
423 - final name = event['name'];  
424 - final data = event['data'];  
425 -  
426 - switch (name) {  
427 - case 'torchState':  
428 - final state = TorchState.values[data as int? ?? 0];  
429 - torchState.value = state;  
430 - case 'zoomScaleState':  
431 - zoomScaleState.value = data as double? ?? 0.0;  
432 - case 'barcode':  
433 - if (data == null) return;  
434 - final parsed = (data as List)  
435 - .map((value) => Barcode.fromNative(value as Map))  
436 - .toList();  
437 -  
438 - final double? width = event['width'] as double?;  
439 - final double? height = event['height'] as double?;  
440 -  
441 - _barcodesController.add(  
442 - BarcodeCapture(  
443 - raw: data,  
444 - barcodes: parsed,  
445 - image: event['image'] as Uint8List?,  
446 - size: width == null || height == null  
447 - ? Size.zero  
448 - : Size(width, height),  
449 - ),  
450 - );  
451 - case 'barcodeMac':  
452 - _barcodesController.add(  
453 - BarcodeCapture(  
454 - raw: data,  
455 - barcodes: [  
456 - Barcode(  
457 - rawValue: (data as Map)['payload'] as String?,  
458 - format: BarcodeFormat.fromRawValue(  
459 - data['symbology'] as int? ?? -1,  
460 - ),  
461 - ),  
462 - ],  
463 - ),  
464 - );  
465 - case 'barcodeWeb':  
466 - final barcode = data as Map?;  
467 - final corners = barcode?['corners'] as List<Object?>? ?? <Object?>[];  
468 -  
469 - _barcodesController.add(  
470 - BarcodeCapture(  
471 - raw: data,  
472 - barcodes: [  
473 - if (barcode != null)  
474 - Barcode(  
475 - rawValue: barcode['rawValue'] as String?,  
476 - rawBytes: barcode['rawBytes'] as Uint8List?,  
477 - format: BarcodeFormat.fromRawValue(  
478 - barcode['format'] as int? ?? -1,  
479 - ),  
480 - corners: List.unmodifiable(  
481 - corners.cast<Map<Object?, Object?>>().map(  
482 - (Map<Object?, Object?> e) {  
483 - return Offset(e['x']! as double, e['y']! as double);  
484 - },  
485 - ),  
486 - ),  
487 - ),  
488 - ],  
489 - ),  
490 - );  
491 - case 'error':  
492 - throw MobileScannerException(  
493 - errorCode: MobileScannerErrorCode.genericError,  
494 - errorDetails: MobileScannerErrorDetails(message: data as String?),  
495 - );  
496 - default:  
497 - throw UnimplementedError(name as String?);  
498 - }  
499 - }  
500 -  
501 - /// updates the native ScanWindow  
502 - Future<void> updateScanWindow(Rect? window) async {  
503 - List? data;  
504 - if (window != null) {  
505 - data = [window.left, window.top, window.right, window.bottom];  
506 - } 171 + @override
  172 + Future<void> dispose() async {
  173 + await MobileScannerPlatform.instance.dispose();
  174 + unawaited(_barcodesController.close());
507 175
508 - await _methodChannel.invokeMethod('updateScanWindow', {'rect': data}); 176 + super.dispose();
509 } 177 }
510 } 178 }