Committed by
GitHub
Merge pull request #1392 from juliansteenbakker/hotfix/usage-after-dispose
fix: black screen after multiple initialization
Showing
8 changed files
with
118 additions
and
68 deletions
| @@ -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,11 +19,12 @@ class ScannerErrorWidget extends StatelessWidget { | @@ -32,11 +19,12 @@ 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, | 22 | + error.errorCode.message, |
| 36 | style: const TextStyle(color: Colors.white), | 23 | style: const TextStyle(color: Colors.white), |
| 37 | ), | 24 | ), |
| 25 | + if (error.errorDetails?.message case final String message) | ||
| 38 | Text( | 26 | Text( |
| 39 | - error.errorDetails?.message ?? '', | 27 | + message, |
| 40 | style: const TextStyle(color: Colors.white), | 28 | style: const TextStyle(color: Colors.white), |
| 41 | ), | 29 | ), |
| 42 | ], | 30 | ], |
| @@ -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(); | 287 | + Future<void> disposeMobileScanner() async { |
| 288 | + if (widget.controller == null) { | ||
| 289 | + WidgetsBinding.instance.removeObserver(this); | ||
| 290 | + } | ||
| 281 | 291 | ||
| 282 | - if (_subscription != null) { | ||
| 283 | - _subscription!.cancel(); | 292 | + await _subscription?.cancel(); |
| 284 | _subscription = null; | 293 | _subscription = null; |
| 285 | - } | ||
| 286 | 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(); |
| 302 | + } | ||
| 297 | } | 303 | } |
| 304 | + | ||
| 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()); | ||
| 298 | } | 316 | } |
| 299 | 317 | ||
| 300 | @override | 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. |
lib/src/objects/scanner_error_widget.dart
0 → 100644
| 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 | } |
-
Please register or login to post a comment