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 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
... ...
... ... @@ -8,19 +8,6 @@ class ScannerErrorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
String errorMessage;
switch (error.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
errorMessage = 'Controller not ready.';
case MobileScannerErrorCode.permissionDenied:
errorMessage = 'Permission denied';
case MobileScannerErrorCode.unsupported:
errorMessage = 'Scanning is unsupported on this device';
default:
errorMessage = 'Generic Error';
}
return ColoredBox(
color: Colors.black,
child: Center(
... ... @@ -32,11 +19,12 @@ class ScannerErrorWidget extends StatelessWidget {
child: Icon(Icons.error, color: Colors.white),
),
Text(
errorMessage,
error.errorCode.message,
style: const TextStyle(color: Colors.white),
),
if (error.errorDetails?.message case final String message)
Text(
error.errorDetails?.message ?? '',
message,
style: const TextStyle(color: Colors.white),
),
],
... ...
... ... @@ -27,6 +27,23 @@ enum MobileScannerErrorCode {
/// Scanning is unsupported on the current device.
unsupported;
String get message {
switch (this) {
case MobileScannerErrorCode.controllerUninitialized:
return 'The MobileScannerController has not been initialized. Call start() before using it.';
case MobileScannerErrorCode.permissionDenied:
return 'Camera permission denied.';
case MobileScannerErrorCode.unsupported:
return 'Scanning is not supported on this device.';
case MobileScannerErrorCode.controllerAlreadyInitialized:
return 'The MobileScannerController is already running. Stop it before starting again.';
case MobileScannerErrorCode.controllerDisposed:
return 'The MobileScannerController was used after it was disposed.';
case MobileScannerErrorCode.genericError:
return 'An unexpected error occurred.';
}
}
/// Convert the given [PlatformException.code] to a [MobileScannerErrorCode].
factory MobileScannerErrorCode.fromPlatformException(
PlatformException exception,
... ...
... ... @@ -80,10 +80,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
);
}
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
throw MobileScannerException(
errorCode: MobileScannerErrorCode.unsupported,
errorDetails: MobileScannerErrorDetails(
message: 'Only Android, iOS and macOS are supported.',
message: MobileScannerErrorCode.unsupported.message,
),
);
}
... ... @@ -218,10 +218,10 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
@override
Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
if (!_pausing && _textureId != null) {
throw const MobileScannerException(
throw MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
message: 'The scanner was already started.',
message: MobileScannerErrorCode.controllerAlreadyInitialized.message,
),
);
}
... ... @@ -339,6 +339,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
@override
Future<void> dispose() async {
await updateScanWindow(null);
await stop();
}
}
... ...
... ... @@ -6,6 +6,7 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
import 'package:mobile_scanner/src/objects/scanner_error_widget.dart';
import 'package:mobile_scanner/src/scan_window_calculation.dart';
/// The function signature for the error builder.
... ... @@ -134,7 +135,7 @@ class MobileScanner extends StatefulWidget {
class _MobileScannerState extends State<MobileScanner>
with WidgetsBindingObserver {
late final controller = widget.controller ?? MobileScannerController();
late final MobileScannerController controller;
/// The current scan window.
Rect? scanWindow;
... ... @@ -207,12 +208,8 @@ class _MobileScannerState extends State<MobileScanner>
}
final MobileScannerException? error = value.error;
if (error != null) {
const Widget defaultError = ColoredBox(
color: Colors.black,
child: Center(child: Icon(Icons.error, color: Colors.white)),
);
final Widget defaultError = ScannerErrorWidget(error: error);
return widget.errorBuilder?.call(context, error, child) ??
defaultError;
... ... @@ -259,9 +256,22 @@ class _MobileScannerState extends State<MobileScanner>
StreamSubscription? _subscription;
@override
void initState() {
if (widget.onDetect != null) {
Future<void> initMobileScanner() async {
// TODO: This will be fixed in another PR
// If debug mode is enabled, stop the controller first before starting it.
// If a hot-restart is initiated, the controller won't be stopped, and because
// there is no way of knowing if a hot-restart has happened, we must assume
// every start is a hot-restart.
// if (kDebugMode) {
// try {
// await controller.stop();
// } catch (e) {
// // Don't do anything if the controller is already stopped.
// debugPrint('$e');
// }
// }
if (widget.controller == null) {
WidgetsBinding.instance.addObserver(this);
_subscription = controller.barcodes.listen(
widget.onDetect,
... ... @@ -270,36 +280,44 @@ class _MobileScannerState extends State<MobileScanner>
);
}
if (controller.autoStart) {
controller.start();
await controller.start();
}
super.initState();
}
@override
void dispose() {
super.dispose();
Future<void> disposeMobileScanner() async {
if (widget.controller == null) {
WidgetsBinding.instance.removeObserver(this);
}
if (_subscription != null) {
_subscription!.cancel();
await _subscription?.cancel();
_subscription = null;
}
if (controller.autoStart) {
controller.stop();
await controller.stop();
}
// When this widget is unmounted, reset the scan window.
unawaited(controller.updateScanWindow(null));
// Dispose default controller if not provided by user
if (widget.controller == null) {
controller.dispose();
WidgetsBinding.instance.removeObserver(this);
await controller.dispose();
}
}
@override
void initState() {
super.initState();
controller = widget.controller ?? MobileScannerController();
unawaited(initMobileScanner());
}
@override
void dispose() {
super.dispose();
unawaited(disposeMobileScanner());
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (widget.controller != null || !controller.value.hasCameraPermission) {
if (!controller.value.hasCameraPermission) {
return;
}
... ... @@ -309,16 +327,8 @@ class _MobileScannerState extends State<MobileScanner>
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
_subscription = controller.barcodes.listen(
widget.onDetect,
onError: widget.onDetectError,
cancelOnError: false,
);
unawaited(controller.start());
case AppLifecycleState.inactive:
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
... ...
... ... @@ -164,20 +164,19 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
void _throwIfNotInitialized() {
if (!value.isInitialized) {
throw const MobileScannerException(
throw MobileScannerException(
errorCode: MobileScannerErrorCode.controllerUninitialized,
errorDetails: MobileScannerErrorDetails(
message: 'The MobileScannerController has not been initialized.',
message: MobileScannerErrorCode.controllerUninitialized.message,
),
);
}
if (_isDisposed) {
throw const MobileScannerException(
throw MobileScannerException(
errorCode: MobileScannerErrorCode.controllerDisposed,
errorDetails: MobileScannerErrorDetails(
message:
'The MobileScannerController was used after it has been disposed.',
message: MobileScannerErrorCode.controllerDisposed.message,
),
);
}
... ... @@ -284,11 +283,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// If the permission is denied on iOS, MacOS or Web, there is no way to request it again.
Future<void> start({CameraFacing? cameraDirection}) async {
if (_isDisposed) {
throw const MobileScannerException(
throw MobileScannerException(
errorCode: MobileScannerErrorCode.controllerDisposed,
errorDetails: MobileScannerErrorDetails(
message:
'The MobileScannerController was used after it has been disposed.',
message: MobileScannerErrorCode.controllerDisposed.message,
),
);
}
... ... @@ -332,13 +330,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
);
}
} on MobileScannerException catch (error) {
// If the controller is already initialized, ignore the error.
// Starting the controller while it is already started, or in the process of starting, is redundant.
if (error.errorCode ==
MobileScannerErrorCode.controllerAlreadyInitialized) {
return;
}
// The initialization finished with an error.
// To avoid stale values, reset the output size,
// torch state and zoom scale to the defaults.
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScannerErrorWidget extends StatelessWidget {
const ScannerErrorWidget({super.key, required this.error});
final MobileScannerException error;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.black,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 16),
child: Icon(Icons.error, color: Colors.white),
),
if (kDebugMode) ...[
Text(
error.errorCode.message,
style: const TextStyle(color: Colors.white),
),
if (error.errorDetails?.message case final String message)
Text(
message,
style: const TextStyle(color: Colors.white),
),
] else
Text(
MobileScannerErrorCode.genericError.message,
style: const TextStyle(color: Colors.white),
),
],
),
),
);
}
}
... ...
... ... @@ -273,10 +273,10 @@ class MobileScannerWeb extends MobileScannerPlatform {
);
}
throw const MobileScannerException(
throw MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
message: 'The scanner was already started.',
message: MobileScannerErrorCode.controllerAlreadyInitialized.message,
),
);
}
... ...