Julian Steenbakker
Committed by GitHub

Merge pull request #373 from navaronbracke/fix_start_error_handling

fix: Fix start error handling
## 3.0.0-beta.3
Deprecated:
* The `onStart` method has been renamed to `onScannerStarted`.
* The `onPermissionSet` argument of the `MobileScannerController` is now deprecated.
Breaking changes:
* `MobileScannerException` now uses an `errorCode` instead of a `message`.
* `MobileScannerException` now contains additional details from the original error.
* Refactored `MobileScannerController.start()` to throw `MobileScannerException`s
with consistent error codes, rather than string messages.
To handle permission errors, consider catching the result of `MobileScannerController.start()`.
* The `autoResume` attribute has been removed from the `MobileScanner` widget.
The controller already automatically resumes, so it had no effect.
* Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef.
Improvements:
* Toggling the device torch now does nothing if the device has no torch, rather than throwing an error.
Features:
* Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder.
* Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically.
Fixed:
* Fixed a memory leak where the `MobileScanner` widget would never close its subscription to the barcode events.
* Fixed a dependency on all properties of `MediaQueryData` to build the preview widget. Now the preview only depends on its layout constraints.
* Fixed a potential crash when the scanner is restarted due to the app being resumed.
* Various documentation improvements.
## 3.0.0-beta.2
Breaking changes:
* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
... ...
... ... @@ -10,6 +10,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
... ...
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
... ...
... ... @@ -17,10 +17,9 @@ import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
typealias PermissionCallback = (permissionGranted: Boolean) -> Unit
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit
typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias MobileScannerErrorCallback = (error: String) -> Unit
... ... @@ -49,10 +48,9 @@ class MobileScanner(
private const val REQUEST_CODE = 0x0786
}
private var listener: PluginRegistry.RequestPermissionsResultListener? = null
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var pendingPermissionResult: MethodChannel.Result? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
... ... @@ -86,16 +84,12 @@ class MobileScanner(
/**
* Request camera permissions.
*/
fun requestPermission(permissionCallback: PermissionCallback) {
listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->
if (requestCode != REQUEST_CODE) {
false
} else {
val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED
permissionCallback(authorized)
true
}
}
fun requestPermission(result: MethodChannel.Result) {
if(pendingPermissionResult != null) {
return
}
pendingPermissionResult = result
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
... ... @@ -108,7 +102,14 @@ class MobileScanner(
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
if (requestCode != REQUEST_CODE) {
return false
}
pendingPermissionResult?.success(grantResults[0] == PackageManager.PERMISSION_GRANTED)
pendingPermissionResult = null
return true
}
/**
... ...
... ... @@ -23,14 +23,8 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
private lateinit var barcodeHandler: BarcodeHandler
private var permissionResult: MethodChannel.Result? = null
private var analyzerResult: MethodChannel.Result? = null
private val permissionCallback: PermissionCallback = {hasPermission: Boolean ->
permissionResult?.success(hasPermission)
permissionResult = null
}
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
if (image != null) {
barcodeHandler.publishEvent(mapOf(
... ... @@ -78,7 +72,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
}
when (call.method) {
"state" -> result.success(handler!!.hasCameraPermission())
"request" -> requestPermission(result)
"request" -> handler!!.requestPermission(result)
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
... ... @@ -124,11 +118,6 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
onDetachedFromActivity()
}
private fun requestPermission(result: MethodChannel.Result) {
permissionResult = result
handler!!.requestPermission(permissionCallback)
}
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
val torch: Boolean = call.argument<Boolean>("torch") ?: false
... ... @@ -208,7 +197,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
handler!!.stop()
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called stop() while already stopped!", null)
result.success(null)
}
}
... ...
... ... @@ -15,13 +15,10 @@ class _BarcodeListScannerWithControllerState
with SingleTickerProviderStateMixin {
BarcodeCapture? barcodeCapture;
MobileScannerController controller = MobileScannerController(
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
... ... @@ -29,6 +26,31 @@ class _BarcodeListScannerWithControllerState
bool isStarted = true;
void _startOrStop() {
if (isStarted) {
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
}
});
}
setState(() {
isStarted = !isStarted;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
... ... @@ -40,17 +62,13 @@ class _BarcodeListScannerWithControllerState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcodeCapture) {
setState(() {
this.barcodeCapture = barcodeCapture;
});
},
onStart: (arguments) {
// Do something with start arguments
onScannerStarted: (arguments) {
// Do something with arguments.
},
),
Align(
... ... @@ -96,10 +114,7 @@ class _BarcodeListScannerWithControllerState
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted ? controller.stop() : controller.start();
isStarted = !isStarted;
}),
onPressed: _startOrStop,
),
Center(
child: SizedBox(
... ... @@ -177,4 +192,10 @@ class _BarcodeListScannerWithControllerState
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
... ...
... ... @@ -15,13 +15,10 @@ class _BarcodeScannerWithControllerState
with SingleTickerProviderStateMixin {
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
... ... @@ -29,6 +26,31 @@ class _BarcodeScannerWithControllerState
bool isStarted = true;
void _startOrStop() {
if (isStarted) {
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
}
});
}
setState(() {
isStarted = !isStarted;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
... ... @@ -40,10 +62,6 @@ class _BarcodeScannerWithControllerState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
... ... @@ -93,10 +111,7 @@ class _BarcodeScannerWithControllerState
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted ? controller.stop() : controller.start();
isStarted = !isStarted;
}),
onPressed: _startOrStop,
),
Center(
child: SizedBox(
... ... @@ -175,4 +190,10 @@ class _BarcodeScannerWithControllerState
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
... ...
... ... @@ -17,13 +17,10 @@ class _BarcodeScannerReturningImageState
BarcodeCapture? barcode;
MobileScannerArguments? arguments;
MobileScannerController controller = MobileScannerController(
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
returnImage: true,
... ... @@ -31,6 +28,31 @@ class _BarcodeScannerReturningImageState
bool isStarted = true;
void _startOrStop() {
if (isStarted) {
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
}
});
}
setState(() {
isStarted = !isStarted;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
... ... @@ -62,10 +84,6 @@ class _BarcodeScannerReturningImageState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
... ... @@ -115,12 +133,7 @@ class _BarcodeScannerReturningImageState
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted
? controller.stop()
: controller.start();
isStarted = !isStarted;
}),
onPressed: _startOrStop,
),
Center(
child: SizedBox(
... ... @@ -171,4 +184,10 @@ class _BarcodeScannerReturningImageState
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
... ...
... ... @@ -107,11 +107,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
private func stop(_ result: @escaping FlutterResult) {
do {
try mobileScanner.stop()
} catch {
result(FlutterError(code: "MobileScanner",
message: "Called stop() while already stopped!",
details: nil))
}
} catch {}
result(nil)
}
... ...
... ... @@ -2,11 +2,13 @@ library mobile_scanner;
export 'src/enums/camera_facing.dart';
export 'src/enums/detection_speed.dart';
export 'src/enums/mobile_scanner_error_code.dart';
export 'src/enums/mobile_scanner_state.dart';
export 'src/enums/ratio.dart';
export 'src/enums/torch_state.dart';
export 'src/mobile_scanner.dart';
export 'src/mobile_scanner_controller.dart';
export 'src/mobile_scanner_exception.dart';
export 'src/objects/barcode.dart';
export 'src/objects/barcode_capture.dart';
export 'src/objects/mobile_scanner_arguments.dart';
... ...
/// This enum defines the different error codes for the mobile scanner.
enum MobileScannerErrorCode {
/// The controller was used
/// while it was not yet initialized using [MobileScannerController.start].
controllerUninitialized,
/// A generic error occurred.
///
/// This error code is used for all errors that do not have a specific error code.
genericError,
/// The permission to use the camera was denied.
permissionDenied,
}
... ...
/// The authorization state of the scanner.
enum MobileScannerState {
/// The scanner has yet to request weather it is [authorized] or [denied]
/// The scanner has not yet requested the required permissions.
undetermined,
/// The scanner has the required permissions.
... ...
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
typedef MobileScannerCallback = void Function(BarcodeCapture barcodes);
typedef MobileScannerArgumentsCallback = void Function(
MobileScannerArguments? arguments,
);
/// A widget showing a live camera preview.
/// The [MobileScanner] widget displays a live camera preview.
class MobileScanner extends StatefulWidget {
/// The controller of the camera.
/// The controller that manages the barcode scanner.
///
/// If this is null, the scanner will manage its own controller.
final MobileScannerController? controller;
/// Calls the provided [onPermissionSet] callback when the permission is set.
// @Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
// ignore: deprecated_consistency
final Function(bool permissionGranted)? onPermissionSet;
/// Function that gets called when a Barcode is detected.
/// The [BoxFit] for the camera preview.
///
/// [barcode] The barcode object with all information about the scanned code.
/// [startInternalArguments] Information about the state of the MobileScanner widget
final MobileScannerCallback onDetect;
/// Defaults to [BoxFit.cover].
final BoxFit fit;
/// Function that gets called when the scanner is started.
///
/// [arguments] The start arguments of the scanner. This contains the size of
/// the scanner which can be used to draw a box over the scanner.
final MobileScannerArgumentsCallback? onStart;
/// The function that signals when new codes were detected by the [controller].
final void Function(BarcodeCapture barcodes) onDetect;
/// Handles how the widget should fit the screen.
final BoxFit fit;
/// The function that signals when the barcode scanner is started.
@Deprecated('Use onScannerStarted() instead.')
final void Function(MobileScannerArguments? arguments)? onStart;
/// Whether to automatically resume the camera when the application is resumed
final bool autoResume;
/// The function that signals when the barcode scanner is started.
final void Function(MobileScannerArguments? arguments)? onScannerStarted;
/// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
/// The function that builds a placeholder widget when the scanner
/// is not yet displaying its camera preview.
///
/// If this is null, a black [ColoredBox] is used as placeholder.
final Widget Function(BuildContext, Widget?)? placeholderBuilder;
/// Create a new [MobileScanner] using the provided [controller]
/// and [onBarcodeDetected] callback.
const MobileScanner({
super.key,
required this.onDetect,
this.onStart,
this.controller,
this.autoResume = true,
this.fit = BoxFit.cover,
@Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
this.onPermissionSet,
required this.onDetect,
@Deprecated('Use onScannerStarted() instead.') this.onStart,
this.onScannerStarted,
this.placeholderBuilder,
super.key,
});
@override
... ... @@ -55,104 +52,109 @@ class MobileScanner extends StatefulWidget {
class _MobileScannerState extends State<MobileScanner>
with WidgetsBindingObserver {
late MobileScannerController controller;
/// The subscription that listens to barcode detection.
StreamSubscription<BarcodeCapture>? _barcodesSubscription;
/// The internally managed controller.
late MobileScannerController _controller;
/// Whether the controller should resume
/// when the application comes back to the foreground.
bool _resumeFromBackground = false;
/// Start the given [scanner].
void _startScanner(MobileScannerController scanner) {
if (!_controller.autoStart) {
debugPrint(
'mobile_scanner: not starting automatically because autoStart is set to false in the controller.',
);
return;
}
scanner.start().then((arguments) {
// ignore: deprecated_member_use_from_same_package
widget.onStart?.call(arguments);
widget.onScannerStarted?.call(arguments);
});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
controller = widget.controller ??
MobileScannerController(onPermissionSet: widget.onPermissionSet);
if (!controller.isStarting) {
_startScanner();
}
}
_controller = widget.controller ?? MobileScannerController();
Future<void> _startScanner() async {
final arguments = await controller.start();
widget.onStart?.call(arguments);
}
_barcodesSubscription = _controller.barcodes.listen(
widget.onDetect,
);
bool resumeFromBackground = false;
if (!_controller.isStarting) {
_startScanner(_controller);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// App state changed before it is initialized.
if (controller.isStarting) {
// App state changed before the controller was initialized.
if (_controller.isStarting) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
resumeFromBackground = false;
_startScanner();
_resumeFromBackground = false;
_startScanner(_controller);
break;
case AppLifecycleState.paused:
resumeFromBackground = true;
_resumeFromBackground = true;
break;
case AppLifecycleState.inactive:
if (!resumeFromBackground) controller.stop();
if (!_resumeFromBackground) {
_controller.stop();
}
break;
default:
case AppLifecycleState.detached:
break;
}
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller.startArguments,
return ValueListenableBuilder<MobileScannerArguments?>(
valueListenable: _controller.startArguments,
builder: (context, value, child) {
value = value as MobileScannerArguments?;
if (value == null) {
return const ColoredBox(color: Colors.black);
} else {
controller.barcodes.listen((barcode) {
widget.onDetect(barcode);
});
return ClipRect(
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: kIsWeb
? HtmlElementView(viewType: value.webId!)
: Texture(textureId: value.textureId!),
),
),
),
);
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
}
return ClipRect(
child: LayoutBuilder(
builder: (_, constraints) {
return SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: kIsWeb
? HtmlElementView(viewType: value.webId!)
: Texture(textureId: value.textureId!),
),
),
);
},
),
);
},
);
}
@override
void didUpdateWidget(covariant MobileScanner oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller == null) {
if (widget.controller != null) {
controller.dispose();
controller = widget.controller!;
}
} else {
if (widget.controller == null) {
controller =
MobileScannerController(onPermissionSet: widget.onPermissionSet);
} else if (oldWidget.controller != widget.controller) {
controller = widget.controller!;
}
}
}
@override
void dispose() {
controller.dispose();
WidgetsBinding.instance.removeObserver(this);
_barcodesSubscription?.cancel();
_controller.dispose();
super.dispose();
}
}
... ...
... ... @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/barcode_utility.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
/// The [MobileScannerController] holds all the logic of this plugin,
/// where as the [MobileScanner] class is the frontend of this plugin.
... ... @@ -18,7 +17,9 @@ class MobileScannerController {
this.torchEnabled = false,
this.formats,
this.returnImage = false,
this.onPermissionSet,
@Deprecated('Instead, use the result of calling `start()` to determine if permissions were granted.')
this.onPermissionSet,
this.autoStart = true,
}) {
// In case a new instance is created before calling dispose()
if (controllerHashcode != null) {
... ... @@ -64,6 +65,9 @@ class MobileScannerController {
/// [DetectionSpeed.normal] (which is the default value).
final int detectionTimeoutMs;
/// Automatically start the mobileScanner on initialization.
final bool autoStart;
/// Sets the barcode stream
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
... ... @@ -74,6 +78,9 @@ class MobileScannerController {
static const EventChannel _eventChannel =
EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
@Deprecated(
'Instead, use the result of calling `start()` to determine if permissions were granted.',
)
Function(bool permissionGranted)? onPermissionSet;
/// Listen to events from the platform specific code
... ... @@ -115,8 +122,14 @@ class MobileScannerController {
return arguments;
}
/// Start barcode scanning. This will first check if the required permissions
/// are set.
/// Start scanning for barcodes.
/// Upon calling this method, the necessary camera permission will be requested.
///
/// Returns an instance of [MobileScannerArguments]
/// when the scanner was successfully started.
/// Returns null if the scanner is currently starting.
///
/// Throws a [MobileScannerException] if starting the scanner failed.
Future<MobileScannerArguments?> start({
CameraFacing? cameraFacingOverride,
}) async {
... ... @@ -124,6 +137,7 @@ class MobileScannerController {
debugPrint("Called start() while starting.");
return null;
}
isStarting = true;
// Check authorization status
... ... @@ -136,16 +150,17 @@ class MobileScannerController {
await _methodChannel.invokeMethod('request') as bool? ?? false;
if (!result) {
isStarting = false;
onPermissionSet?.call(result);
throw MobileScannerException('User declined camera permission.');
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
}
break;
case MobileScannerState.denied:
isStarting = false;
onPermissionSet?.call(false);
throw MobileScannerException('User declined camera permission.');
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
case MobileScannerState.authorized:
onPermissionSet?.call(true);
break;
}
}
... ... @@ -158,18 +173,27 @@ class MobileScannerController {
_argumentsToMap(cameraFacingOverride: cameraFacingOverride),
);
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
if (error.code == "MobileScannerWeb") {
onPermissionSet?.call(false);
errorCode = MobileScannerErrorCode.permissionDenied;
}
isStarting = false;
return null;
throw MobileScannerException(
errorCode: errorCode,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
if (startResult == null) {
isStarting = false;
throw MobileScannerException(
'Failed to start mobileScanner, no response from platform side',
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
);
}
... ... @@ -178,13 +202,6 @@ class MobileScannerController {
torchState.value = TorchState.on;
}
if (kIsWeb) {
// If we reach this line, it means camera permission has been granted
onPermissionSet?.call(
true,
);
}
isStarting = false;
return startArguments.value = MobileScannerArguments(
size: kIsWeb
... ... @@ -210,14 +227,18 @@ class MobileScannerController {
/// Switches the torch on or off.
///
/// Only works if torch is available.
/// Does nothing if the device has no torch.
///
/// Throws if the controller was not initialized.
Future<void> toggleTorch() async {
if (_hasTorch == null) {
throw MobileScannerException(
'Cannot toggle torch if start() has never been called',
final hasTorch = _hasTorch;
if (hasTorch == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerUninitialized,
);
} else if (!_hasTorch!) {
throw MobileScannerException('Device has no torch');
} else if (!hasTorch) {
return;
}
torchState.value =
... ... @@ -258,7 +279,6 @@ class MobileScannerController {
_barcodesController.close();
if (hashCode == controllerHashcode) {
controllerHashcode = null;
onPermissionSet = null;
}
}
... ... @@ -307,7 +327,10 @@ class MobileScannerController {
);
break;
case 'error':
throw MobileScannerException(data as String);
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(message: data as String?),
);
default:
throw UnimplementedError(name as String?);
}
... ...
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
/// This class represents an exception thrown by the mobile scanner.
class MobileScannerException implements Exception {
String message;
MobileScannerException(this.message);
const MobileScannerException({
required this.errorCode,
this.errorDetails,
});
/// The error code of the exception.
final MobileScannerErrorCode errorCode;
/// The additional error details that came with the [errorCode].
final MobileScannerErrorDetails? errorDetails;
}
/// The raw error details for a [MobileScannerException].
class MobileScannerErrorDetails {
const MobileScannerErrorDetails({
this.code,
this.details,
this.message,
});
/// The error code from the [PlatformException].
final String? code;
/// The details from the [PlatformException].
final Object? details;
/// The error message from the [PlatformException].
final String? message;
}
... ...
... ... @@ -260,9 +260,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
func stop(_ result: FlutterResult) {
if (device == nil) {
result(FlutterError(code: "MobileScanner",
message: "Called stop() while already stopped!",
details: nil))
result(nil)
return
}
captureSession.stopRunning()
... ...
name: mobile_scanner
description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
version: 3.0.0-beta.2
version: 3.0.0-beta.3
repository: https://github.com/juliansteenbakker/mobile_scanner
environment:
... ...