Julian Steenbakker
Committed by GitHub

Merge pull request #1392 from juliansteenbakker/hotfix/usage-after-dispose

fix: black screen after multiple initialization
@@ -59,6 +59,7 @@ @@ -59,6 +59,7 @@
59 ignoresPersistentStateOnLaunch = "NO" 59 ignoresPersistentStateOnLaunch = "NO"
60 debugDocumentVersioning = "YES" 60 debugDocumentVersioning = "YES"
61 debugServiceExtension = "internal" 61 debugServiceExtension = "internal"
  62 + enableGPUValidationMode = "1"
62 allowLocationSimulation = "YES"> 63 allowLocationSimulation = "YES">
63 <BuildableProductRunnable 64 <BuildableProductRunnable
64 runnableDebuggingMode = "0"> 65 runnableDebuggingMode = "0">
@@ -8,19 +8,6 @@ class ScannerErrorWidget extends StatelessWidget { @@ -8,19 +8,6 @@ class ScannerErrorWidget extends StatelessWidget {
8 8
9 @override 9 @override
10 Widget build(BuildContext context) { 10 Widget build(BuildContext context) {
11 - String errorMessage;  
12 -  
13 - switch (error.errorCode) {  
14 - case MobileScannerErrorCode.controllerUninitialized:  
15 - errorMessage = 'Controller not ready.';  
16 - case MobileScannerErrorCode.permissionDenied:  
17 - errorMessage = 'Permission denied';  
18 - case MobileScannerErrorCode.unsupported:  
19 - errorMessage = 'Scanning is unsupported on this device';  
20 - default:  
21 - errorMessage = 'Generic Error';  
22 - }  
23 -  
24 return ColoredBox( 11 return ColoredBox(
25 color: Colors.black, 12 color: Colors.black,
26 child: Center( 13 child: Center(
@@ -32,13 +19,14 @@ class ScannerErrorWidget extends StatelessWidget { @@ -32,13 +19,14 @@ class ScannerErrorWidget extends StatelessWidget {
32 child: Icon(Icons.error, color: Colors.white), 19 child: Icon(Icons.error, color: Colors.white),
33 ), 20 ),
34 Text( 21 Text(
35 - errorMessage,  
36 - style: const TextStyle(color: Colors.white),  
37 - ),  
38 - Text(  
39 - error.errorDetails?.message ?? '', 22 + error.errorCode.message,
40 style: const TextStyle(color: Colors.white), 23 style: const TextStyle(color: Colors.white),
41 ), 24 ),
  25 + if (error.errorDetails?.message case final String message)
  26 + Text(
  27 + message,
  28 + style: const TextStyle(color: Colors.white),
  29 + ),
42 ], 30 ],
43 ), 31 ),
44 ), 32 ),
@@ -27,6 +27,23 @@ enum MobileScannerErrorCode { @@ -27,6 +27,23 @@ enum MobileScannerErrorCode {
27 /// Scanning is unsupported on the current device. 27 /// Scanning is unsupported on the current device.
28 unsupported; 28 unsupported;
29 29
  30 + String get message {
  31 + switch (this) {
  32 + case MobileScannerErrorCode.controllerUninitialized:
  33 + return 'The MobileScannerController has not been initialized. Call start() before using it.';
  34 + case MobileScannerErrorCode.permissionDenied:
  35 + return 'Camera permission denied.';
  36 + case MobileScannerErrorCode.unsupported:
  37 + return 'Scanning is not supported on this device.';
  38 + case MobileScannerErrorCode.controllerAlreadyInitialized:
  39 + return 'The MobileScannerController is already running. Stop it before starting again.';
  40 + case MobileScannerErrorCode.controllerDisposed:
  41 + return 'The MobileScannerController was used after it was disposed.';
  42 + case MobileScannerErrorCode.genericError:
  43 + return 'An unexpected error occurred.';
  44 + }
  45 + }
  46 +
30 /// Convert the given [PlatformException.code] to a [MobileScannerErrorCode]. 47 /// Convert the given [PlatformException.code] to a [MobileScannerErrorCode].
31 factory MobileScannerErrorCode.fromPlatformException( 48 factory MobileScannerErrorCode.fromPlatformException(
32 PlatformException exception, 49 PlatformException exception,
@@ -80,10 +80,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -80,10 +80,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
80 ); 80 );
81 } 81 }
82 82
83 - throw const MobileScannerException(  
84 - errorCode: MobileScannerErrorCode.genericError, 83 + throw MobileScannerException(
  84 + errorCode: MobileScannerErrorCode.unsupported,
85 errorDetails: MobileScannerErrorDetails( 85 errorDetails: MobileScannerErrorDetails(
86 - message: 'Only Android, iOS and macOS are supported.', 86 + message: MobileScannerErrorCode.unsupported.message,
87 ), 87 ),
88 ); 88 );
89 } 89 }
@@ -218,10 +218,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -218,10 +218,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
218 @override 218 @override
219 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { 219 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
220 if (!_pausing && _textureId != null) { 220 if (!_pausing && _textureId != null) {
221 - throw const MobileScannerException( 221 + throw MobileScannerException(
222 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, 222 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
223 errorDetails: MobileScannerErrorDetails( 223 errorDetails: MobileScannerErrorDetails(
224 - message: 'The scanner was already started.', 224 + message: MobileScannerErrorCode.controllerAlreadyInitialized.message,
225 ), 225 ),
226 ); 226 );
227 } 227 }
@@ -339,6 +339,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -339,6 +339,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
339 339
340 @override 340 @override
341 Future<void> dispose() async { 341 Future<void> dispose() async {
  342 + await updateScanWindow(null);
342 await stop(); 343 await stop();
343 } 344 }
344 } 345 }
@@ -6,6 +6,7 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; @@ -6,6 +6,7 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
6 import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; 6 import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
7 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 7 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
8 import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; 8 import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
  9 +import 'package:mobile_scanner/src/objects/scanner_error_widget.dart';
9 import 'package:mobile_scanner/src/scan_window_calculation.dart'; 10 import 'package:mobile_scanner/src/scan_window_calculation.dart';
10 11
11 /// The function signature for the error builder. 12 /// The function signature for the error builder.
@@ -134,7 +135,7 @@ class MobileScanner extends StatefulWidget { @@ -134,7 +135,7 @@ class MobileScanner extends StatefulWidget {
134 135
135 class _MobileScannerState extends State<MobileScanner> 136 class _MobileScannerState extends State<MobileScanner>
136 with WidgetsBindingObserver { 137 with WidgetsBindingObserver {
137 - late final controller = widget.controller ?? MobileScannerController(); 138 + late final MobileScannerController controller;
138 139
139 /// The current scan window. 140 /// The current scan window.
140 Rect? scanWindow; 141 Rect? scanWindow;
@@ -207,12 +208,8 @@ class _MobileScannerState extends State<MobileScanner> @@ -207,12 +208,8 @@ class _MobileScannerState extends State<MobileScanner>
207 } 208 }
208 209
209 final MobileScannerException? error = value.error; 210 final MobileScannerException? error = value.error;
210 -  
211 if (error != null) { 211 if (error != null) {
212 - const Widget defaultError = ColoredBox(  
213 - color: Colors.black,  
214 - child: Center(child: Icon(Icons.error, color: Colors.white)),  
215 - ); 212 + final Widget defaultError = ScannerErrorWidget(error: error);
216 213
217 return widget.errorBuilder?.call(context, error, child) ?? 214 return widget.errorBuilder?.call(context, error, child) ??
218 defaultError; 215 defaultError;
@@ -259,9 +256,22 @@ class _MobileScannerState extends State<MobileScanner> @@ -259,9 +256,22 @@ class _MobileScannerState extends State<MobileScanner>
259 256
260 StreamSubscription? _subscription; 257 StreamSubscription? _subscription;
261 258
262 - @override  
263 - void initState() {  
264 - if (widget.onDetect != null) { 259 + Future<void> initMobileScanner() async {
  260 + // TODO: This will be fixed in another PR
  261 + // If debug mode is enabled, stop the controller first before starting it.
  262 + // If a hot-restart is initiated, the controller won't be stopped, and because
  263 + // there is no way of knowing if a hot-restart has happened, we must assume
  264 + // every start is a hot-restart.
  265 + // if (kDebugMode) {
  266 + // try {
  267 + // await controller.stop();
  268 + // } catch (e) {
  269 + // // Don't do anything if the controller is already stopped.
  270 + // debugPrint('$e');
  271 + // }
  272 + // }
  273 +
  274 + if (widget.controller == null) {
265 WidgetsBinding.instance.addObserver(this); 275 WidgetsBinding.instance.addObserver(this);
266 _subscription = controller.barcodes.listen( 276 _subscription = controller.barcodes.listen(
267 widget.onDetect, 277 widget.onDetect,
@@ -270,36 +280,44 @@ class _MobileScannerState extends State<MobileScanner> @@ -270,36 +280,44 @@ class _MobileScannerState extends State<MobileScanner>
270 ); 280 );
271 } 281 }
272 if (controller.autoStart) { 282 if (controller.autoStart) {
273 - controller.start(); 283 + await controller.start();
274 } 284 }
275 - super.initState();  
276 } 285 }
277 286
278 - @override  
279 - void dispose() {  
280 - super.dispose();  
281 -  
282 - if (_subscription != null) {  
283 - _subscription!.cancel();  
284 - _subscription = null; 287 + Future<void> disposeMobileScanner() async {
  288 + if (widget.controller == null) {
  289 + WidgetsBinding.instance.removeObserver(this);
285 } 290 }
286 291
  292 + await _subscription?.cancel();
  293 + _subscription = null;
  294 +
287 if (controller.autoStart) { 295 if (controller.autoStart) {
288 - controller.stop(); 296 + await controller.stop();
289 } 297 }
290 - // When this widget is unmounted, reset the scan window.  
291 - unawaited(controller.updateScanWindow(null));  
292 298
293 // Dispose default controller if not provided by user 299 // Dispose default controller if not provided by user
294 if (widget.controller == null) { 300 if (widget.controller == null) {
295 - controller.dispose();  
296 - WidgetsBinding.instance.removeObserver(this); 301 + await controller.dispose();
297 } 302 }
298 } 303 }
299 304
300 @override 305 @override
  306 + void initState() {
  307 + super.initState();
  308 + controller = widget.controller ?? MobileScannerController();
  309 + unawaited(initMobileScanner());
  310 + }
  311 +
  312 + @override
  313 + void dispose() {
  314 + super.dispose();
  315 + unawaited(disposeMobileScanner());
  316 + }
  317 +
  318 + @override
301 void didChangeAppLifecycleState(AppLifecycleState state) { 319 void didChangeAppLifecycleState(AppLifecycleState state) {
302 - if (widget.controller != null || !controller.value.hasCameraPermission) { 320 + if (!controller.value.hasCameraPermission) {
303 return; 321 return;
304 } 322 }
305 323
@@ -309,16 +327,8 @@ class _MobileScannerState extends State<MobileScanner> @@ -309,16 +327,8 @@ class _MobileScannerState extends State<MobileScanner>
309 case AppLifecycleState.paused: 327 case AppLifecycleState.paused:
310 return; 328 return;
311 case AppLifecycleState.resumed: 329 case AppLifecycleState.resumed:
312 - _subscription = controller.barcodes.listen(  
313 - widget.onDetect,  
314 - onError: widget.onDetectError,  
315 - cancelOnError: false,  
316 - );  
317 -  
318 unawaited(controller.start()); 330 unawaited(controller.start());
319 case AppLifecycleState.inactive: 331 case AppLifecycleState.inactive:
320 - unawaited(_subscription?.cancel());  
321 - _subscription = null;  
322 unawaited(controller.stop()); 332 unawaited(controller.stop());
323 } 333 }
324 } 334 }
@@ -164,20 +164,19 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -164,20 +164,19 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
164 164
165 void _throwIfNotInitialized() { 165 void _throwIfNotInitialized() {
166 if (!value.isInitialized) { 166 if (!value.isInitialized) {
167 - throw const MobileScannerException( 167 + throw MobileScannerException(
168 errorCode: MobileScannerErrorCode.controllerUninitialized, 168 errorCode: MobileScannerErrorCode.controllerUninitialized,
169 errorDetails: MobileScannerErrorDetails( 169 errorDetails: MobileScannerErrorDetails(
170 - message: 'The MobileScannerController has not been initialized.', 170 + message: MobileScannerErrorCode.controllerUninitialized.message,
171 ), 171 ),
172 ); 172 );
173 } 173 }
174 174
175 if (_isDisposed) { 175 if (_isDisposed) {
176 - throw const MobileScannerException( 176 + throw MobileScannerException(
177 errorCode: MobileScannerErrorCode.controllerDisposed, 177 errorCode: MobileScannerErrorCode.controllerDisposed,
178 errorDetails: MobileScannerErrorDetails( 178 errorDetails: MobileScannerErrorDetails(
179 - message:  
180 - 'The MobileScannerController was used after it has been disposed.', 179 + message: MobileScannerErrorCode.controllerDisposed.message,
181 ), 180 ),
182 ); 181 );
183 } 182 }
@@ -284,11 +283,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -284,11 +283,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
284 /// If the permission is denied on iOS, MacOS or Web, there is no way to request it again. 283 /// If the permission is denied on iOS, MacOS or Web, there is no way to request it again.
285 Future<void> start({CameraFacing? cameraDirection}) async { 284 Future<void> start({CameraFacing? cameraDirection}) async {
286 if (_isDisposed) { 285 if (_isDisposed) {
287 - throw const MobileScannerException( 286 + throw MobileScannerException(
288 errorCode: MobileScannerErrorCode.controllerDisposed, 287 errorCode: MobileScannerErrorCode.controllerDisposed,
289 errorDetails: MobileScannerErrorDetails( 288 errorDetails: MobileScannerErrorDetails(
290 - message:  
291 - 'The MobileScannerController was used after it has been disposed.', 289 + message: MobileScannerErrorCode.controllerDisposed.message,
292 ), 290 ),
293 ); 291 );
294 } 292 }
@@ -332,13 +330,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -332,13 +330,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
332 ); 330 );
333 } 331 }
334 } on MobileScannerException catch (error) { 332 } on MobileScannerException catch (error) {
335 - // If the controller is already initialized, ignore the error.  
336 - // Starting the controller while it is already started, or in the process of starting, is redundant.  
337 - if (error.errorCode ==  
338 - MobileScannerErrorCode.controllerAlreadyInitialized) {  
339 - return;  
340 - }  
341 -  
342 // The initialization finished with an error. 333 // The initialization finished with an error.
343 // To avoid stale values, reset the output size, 334 // To avoid stale values, reset the output size,
344 // torch state and zoom scale to the defaults. 335 // torch state and zoom scale to the defaults.
  1 +import 'package:flutter/foundation.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:mobile_scanner/mobile_scanner.dart';
  4 +
  5 +class ScannerErrorWidget extends StatelessWidget {
  6 + const ScannerErrorWidget({super.key, required this.error});
  7 +
  8 + final MobileScannerException error;
  9 +
  10 + @override
  11 + Widget build(BuildContext context) {
  12 + return ColoredBox(
  13 + color: Colors.black,
  14 + child: Center(
  15 + child: Column(
  16 + mainAxisSize: MainAxisSize.min,
  17 + children: [
  18 + const Padding(
  19 + padding: EdgeInsets.only(bottom: 16),
  20 + child: Icon(Icons.error, color: Colors.white),
  21 + ),
  22 + if (kDebugMode) ...[
  23 + Text(
  24 + error.errorCode.message,
  25 + style: const TextStyle(color: Colors.white),
  26 + ),
  27 + if (error.errorDetails?.message case final String message)
  28 + Text(
  29 + message,
  30 + style: const TextStyle(color: Colors.white),
  31 + ),
  32 + ] else
  33 + Text(
  34 + MobileScannerErrorCode.genericError.message,
  35 + style: const TextStyle(color: Colors.white),
  36 + ),
  37 + ],
  38 + ),
  39 + ),
  40 + );
  41 + }
  42 +}
@@ -273,10 +273,10 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -273,10 +273,10 @@ class MobileScannerWeb extends MobileScannerPlatform {
273 ); 273 );
274 } 274 }
275 275
276 - throw const MobileScannerException( 276 + throw MobileScannerException(
277 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, 277 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
278 errorDetails: MobileScannerErrorDetails( 278 errorDetails: MobileScannerErrorDetails(
279 - message: 'The scanner was already started.', 279 + message: MobileScannerErrorCode.controllerAlreadyInitialized.message,
280 ), 280 ),
281 ); 281 );
282 } 282 }