Navaron Bracke
Committed by GitHub

Merge pull request #916 from navaronbracke/mobile_scanner_platform_interface

feat: Mobile scanner platform interface
Showing 38 changed files with 1940 additions and 1976 deletions

Too many changes to show.

To preserve performance only 38 of 38+ files are displayed.

@@ -36,7 +36,7 @@ jobs: @@ -36,7 +36,7 @@ jobs:
36 - uses: subosito/flutter-action@v2.12.0 36 - uses: subosito/flutter-action@v2.12.0
37 with: 37 with:
38 cache: true 38 cache: true
39 - flutter-version: '3.13' 39 + flutter-version: '3.19'
40 channel: 'stable' 40 channel: 'stable'
41 - name: Version 41 - name: Version
42 run: flutter doctor -v 42 run: flutter doctor -v
  1 +## 5.0.0-beta.1
  2 +
  3 +**BREAKING CHANGES:**
  4 +
  5 +* Flutter 3.19.0 is now required.
  6 +* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`.
  7 +* The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion.
  8 +* The `MobileScannerArguments` class has been removed from the public API, as it is an internal type.
  9 +* The `cameraFacingOverride` named argument for the `start()` method has been renamed to `cameraDirection`.
  10 +* The `analyzeImage` function now correctly returns a `BarcodeCapture?` instead of a boolean.
  11 +* The `formats` attribute of the `MobileScannerController` is now non-null.
  12 +* The `MobileScannerState` enum has been renamed to `MobileScannerAuthorizationState`.
  13 +* The various `ValueNotifier`s for the camera state have been removed. Use the `value` of the `MobileScannerController` instead.
  14 +* The `hasTorch` getter has been removed. Instead, use the torch state of the controller's value.
  15 + The `TorchState` enum now provides a new value for unavailable flashlights.
  16 +* The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand.
  17 +* A controller is now required for the `MobileScanner` widget.
  18 +* The `onPermissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`.
  19 +* The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers.
  20 +* The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly.
  21 +* The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay.
  22 +* The torch can no longer be toggled on the web, as this is only available for image tracks and not video tracks. As a result the torch state for the web will always be `TorchState.unavailable`.
  23 +* The zoom scale can no longer be modified on the web, as this is only available for image tracks and not video tracks. As a result, the zoom scale will always be `1.0`.
  24 +
  25 +Improvements:
  26 +* The `MobileScannerController` is now a ChangeNotifier, with `MobileScannerState` as its model.
  27 +* The web implementation now supports alternate URLs for loading the barcode library.
  28 +
1 ## 4.0.1 29 ## 4.0.1
2 Bugs fixed: 30 Bugs fixed:
3 * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !) 31 * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !)
4 32
5 ## 4.0.0 33 ## 4.0.0
6 -BREAKING CHANGES: 34 +
  35 +**BREAKING CHANGES:**
  36 +
7 * [Android] compileSdk has been upgraded to version 34. 37 * [Android] compileSdk has been upgraded to version 34.
8 * [Android] Java version has been upgraded to version 17. 38 * [Android] Java version has been upgraded to version 17.
9 39
@@ -186,7 +216,8 @@ Deprecated: @@ -186,7 +216,8 @@ Deprecated:
186 * The `onStart` method has been renamed to `onScannerStarted`. 216 * The `onStart` method has been renamed to `onScannerStarted`.
187 * The `onPermissionSet` argument of the `MobileScannerController` is now deprecated. 217 * The `onPermissionSet` argument of the `MobileScannerController` is now deprecated.
188 218
189 -Breaking changes: 219 +**BREAKING CHANGES:**
  220 +
190 * `MobileScannerException` now uses an `errorCode` instead of a `message`. 221 * `MobileScannerException` now uses an `errorCode` instead of a `message`.
191 * `MobileScannerException` now contains additional details from the original error. 222 * `MobileScannerException` now contains additional details from the original error.
192 * Refactored `MobileScannerController.start()` to throw `MobileScannerException`s 223 * Refactored `MobileScannerController.start()` to throw `MobileScannerException`s
@@ -223,7 +254,9 @@ Fixes: @@ -223,7 +254,9 @@ Fixes:
223 * [iOS] Fix crash when changing torch state 254 * [iOS] Fix crash when changing torch state
224 255
225 ## 3.0.0-beta.2 256 ## 3.0.0-beta.2
226 -Breaking changes: 257 +
  258 +**BREAKING CHANGES:**
  259 +
227 * The arguments parameter of onDetect is removed. The data is now returned by the onStart callback 260 * The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
228 in the MobileScanner widget. 261 in the MobileScanner widget.
229 * onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image. 262 * onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image.
@@ -243,7 +276,9 @@ Other improvements: @@ -243,7 +276,9 @@ Other improvements:
243 * [iOS] Updated POD dependencies 276 * [iOS] Updated POD dependencies
244 277
245 ## 3.0.0-beta.1 278 ## 3.0.0-beta.1
246 -Breaking changes: 279 +
  280 +**BREAKING CHANGES:**
  281 +
247 * [Android] SDK updated to SDK 33. 282 * [Android] SDK updated to SDK 33.
248 283
249 Features: 284 Features:
@@ -259,7 +294,9 @@ Other changes: @@ -259,7 +294,9 @@ Other changes:
259 * Several minor code improvements 294 * Several minor code improvements
260 295
261 ## 2.0.0 296 ## 2.0.0
262 -Breaking changes: 297 +
  298 +**BREAKING CHANGES:**
  299 +
263 This version is only compatible with flutter 3.0.0 and later. 300 This version is only compatible with flutter 3.0.0 and later.
264 301
265 ## 1.1.2-play-services 302 ## 1.1.2-play-services
@@ -293,7 +330,9 @@ Bugfixes: @@ -293,7 +330,9 @@ Bugfixes:
293 * Upgraded several dependencies. 330 * Upgraded several dependencies.
294 331
295 ## 1.0.0 332 ## 1.0.0
296 -BREAKING CHANGES: 333 +
  334 +**BREAKING CHANGES:**
  335 +
297 This version adds a new allowDuplicates option which now defaults to FALSE. this means that it will only call onDetect once after a scan. 336 This version adds a new allowDuplicates option which now defaults to FALSE. this means that it will only call onDetect once after a scan.
298 If you still want duplicates, you can set allowDuplicates to true. 337 If you still want duplicates, you can set allowDuplicates to true.
299 This also means that you don't have to check for duplicates yourself anymore. 338 This also means that you don't have to check for duplicates yourself anymore.
@@ -7,17 +7,15 @@ @@ -7,17 +7,15 @@
7 7
8 A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. 8 A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS.
9 9
10 -  
11 ## Features Supported 10 ## Features Supported
12 11
13 See the example app for detailed implementation information. 12 See the example app for detailed implementation information.
14 13
15 | Features | Android | iOS | macOS | Web | 14 | Features | Android | iOS | macOS | Web |
16 -|------------------------|--------------------|--------------------|-------|-----| 15 +|------------------------|--------------------|--------------------|----------------------|-----|
17 | analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | 16 | analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
18 | returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | 17 | returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
19 -| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |  
20 -| barcodeOverlay | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | 18 +| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
21 19
22 ## Platform Support 20 ## Platform Support
23 21
@@ -26,6 +24,7 @@ See the example app for detailed implementation information. @@ -26,6 +24,7 @@ See the example app for detailed implementation information.
26 | ✔ | ✔ | ✔ | ✔ | :x: | :x: | 24 | ✔ | ✔ | ✔ | ✔ | :x: | :x: |
27 25
28 ## Platform specific setup 26 ## Platform specific setup
  27 +
29 ### Android 28 ### Android
30 This package uses by default the **bundled version** of MLKit Barcode-scanning for Android. This version is immediately available to the device. But it will increase the size of the app by approximately 3 to 10 MB. 29 This package uses by default the **bundled version** of MLKit Barcode-scanning for Android. This version is immediately available to the device. But it will increase the size of the app by approximately 3 to 10 MB.
31 30
@@ -61,194 +60,110 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: @@ -61,194 +60,110 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities:
61 <img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png"> 60 <img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png">
62 61
63 ## Web 62 ## Web
64 -This package uses ZXing on web to read barcodes so it needs to be included in `index.html` as script.  
65 -```html  
66 -<script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script>  
67 -```  
68 -  
69 -## Usage  
70 -  
71 -Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller.  
72 63
73 -If you don't provide a controller, you can't control functions like the torch(flash) or switching camera. 64 +As of version 5.0.0 adding the library to the `index.html` is no longer required,
  65 +as the library is automatically loaded on first use.
74 66
75 -If you don't set `detectionSpeed` to `DetectionSpeed.noDuplicates`, you can get multiple scans in a very short time, causing things like pop() to fire lots of times. 67 +### Providing a mirror for the barcode scanning library
76 68
77 -Example without controller: 69 +If a different mirror is needed to load the barcode scanning library,
  70 +the source URL can be set beforehand.
78 71
79 ```dart 72 ```dart
  73 +import 'package:flutter/foundation.dart';
80 import 'package:mobile_scanner/mobile_scanner.dart'; 74 import 'package:mobile_scanner/mobile_scanner.dart';
81 75
82 - @override  
83 - Widget build(BuildContext context) {  
84 - return Scaffold(  
85 - appBar: AppBar(title: const Text('Mobile Scanner')),  
86 - body: MobileScanner(  
87 - // fit: BoxFit.contain,  
88 - onDetect: (capture) {  
89 - final List<Barcode> barcodes = capture.barcodes;  
90 - final Uint8List? image = capture.image;  
91 - for (final barcode in barcodes) {  
92 - debugPrint('Barcode found! ${barcode.rawValue}');  
93 - }  
94 - },  
95 - ),  
96 - );  
97 - } 76 +final String scriptUrl = // ...
  77 +
  78 +if (kIsWeb) {
  79 + MobileScannerPlatform.instance.setBarcodeLibraryScriptUrl(scriptUrl);
  80 +}
98 ``` 81 ```
99 82
100 -Example with controller and initial values: 83 +## Usage
  84 +
  85 +Import the package with `package:mobile_scanner/mobile_scanner.dart`.
  86 +
  87 +Create a new `MobileScannerController` controller, using the required options.
  88 +Provide a `StreamSubscription` for the barcode events.
101 89
102 ```dart 90 ```dart
103 -import 'package:mobile_scanner/mobile_scanner.dart'; 91 +final MobileScannerController controller = MobileScannerController(
  92 + // required options for the scanner
  93 +);
104 94
105 - @override  
106 - Widget build(BuildContext context) {  
107 - return Scaffold(  
108 - appBar: AppBar(title: const Text('Mobile Scanner')),  
109 - body: MobileScanner(  
110 - // fit: BoxFit.contain,  
111 - controller: MobileScannerController(  
112 - detectionSpeed: DetectionSpeed.normal,  
113 - facing: CameraFacing.front,  
114 - torchEnabled: true,  
115 - ),  
116 - onDetect: (capture) {  
117 - final List<Barcode> barcodes = capture.barcodes;  
118 - final Uint8List? image = capture.image;  
119 - for (final barcode in barcodes) {  
120 - debugPrint('Barcode found! ${barcode.rawValue}');  
121 - }  
122 - },  
123 - ),  
124 - );  
125 - } 95 +StreamSubscription<Object?>? _subscription;
126 ``` 96 ```
127 97
128 -Example with controller and torch & camera controls: 98 +Ensure that your `State` class mixes in `WidgetsBindingObserver`, to handle lifecyle changes:
129 99
130 ```dart 100 ```dart
131 -import 'package:mobile_scanner/mobile_scanner.dart';  
132 -  
133 - MobileScannerController cameraController = MobileScannerController(); 101 +class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
  102 + // ...
134 103
135 @override 104 @override
136 - Widget build(BuildContext context) {  
137 - return Scaffold(  
138 - appBar: AppBar(  
139 - title: const Text('Mobile Scanner'),  
140 - actions: [  
141 - IconButton(  
142 - color: Colors.white,  
143 - icon: ValueListenableBuilder(  
144 - valueListenable: cameraController.torchState,  
145 - builder: (context, state, child) {  
146 - switch (state as TorchState) {  
147 - case TorchState.off:  
148 - return const Icon(Icons.flash_off, color: Colors.grey);  
149 - case TorchState.on:  
150 - return const Icon(Icons.flash_on, color: Colors.yellow);  
151 - }  
152 - },  
153 - ),  
154 - iconSize: 32.0,  
155 - onPressed: () => cameraController.toggleTorch(),  
156 - ),  
157 - IconButton(  
158 - color: Colors.white,  
159 - icon: ValueListenableBuilder(  
160 - valueListenable: cameraController.cameraFacingState,  
161 - builder: (context, state, child) {  
162 - switch (state as CameraFacing) {  
163 - case CameraFacing.front:  
164 - return const Icon(Icons.camera_front);  
165 - case CameraFacing.back:  
166 - return const Icon(Icons.camera_rear); 105 + void didChangeAppLifecycleState(AppLifecycleState state) {
  106 + super.didChangeAppLifecycleState(state);
  107 +
  108 + switch (state) {
  109 + case AppLifecycleState.detached:
  110 + case AppLifecycleState.hidden:
  111 + case AppLifecycleState.paused:
  112 + return;
  113 + case AppLifecycleState.resumed:
  114 + // Restart the scanner when the app is resumed.
  115 + // Don't forget to resume listening to the barcode events.
  116 + _subscription = controller.barcodes.listen(_handleBarcode);
  117 +
  118 + unawaited(controller.start());
  119 + case AppLifecycleState.inactive:
  120 + // Stop the scanner when the app is paused.
  121 + // Also stop the barcode events subscription.
  122 + unawaited(_subscription?.cancel());
  123 + _subscription = null;
  124 + unawaited(controller.stop());
167 } 125 }
168 - },  
169 - ),  
170 - iconSize: 32.0,  
171 - onPressed: () => cameraController.switchCamera(),  
172 - ),  
173 - ],  
174 - ),  
175 - body: MobileScanner(  
176 - // fit: BoxFit.contain,  
177 - controller: cameraController,  
178 - onDetect: (capture) {  
179 - final List<Barcode> barcodes = capture.barcodes;  
180 - final Uint8List? image = capture.image;  
181 - for (final barcode in barcodes) {  
182 - debugPrint('Barcode found! ${barcode.rawValue}');  
183 - }  
184 - },  
185 - ),  
186 - );  
187 } 126 }
  127 +
  128 + // ...
  129 +}
188 ``` 130 ```
189 131
190 -Example with controller and returning images 132 +Then, start the scanner in `void initState()`:
191 133
192 ```dart 134 ```dart
193 -import 'package:mobile_scanner/mobile_scanner.dart'; 135 +@override
  136 +void initState() {
  137 + super.initState();
  138 + // Start listening to lifecycle changes.
  139 + WidgetsBinding.instance.addObserver(this);
  140 +
  141 + // Start listening to the barcode events.
  142 + _subscription = controller.barcodes.listen(_handleBarcode);
  143 +
  144 + // Finally, start the scanner itself.
  145 + unawaited(controller.start());
  146 +}
  147 +```
194 148
195 - @override  
196 - Widget build(BuildContext context) {  
197 - return Scaffold(  
198 - appBar: AppBar(title: const Text('Mobile Scanner')),  
199 - body: MobileScanner(  
200 - fit: BoxFit.contain,  
201 - controller: MobileScannerController(  
202 - // facing: CameraFacing.back,  
203 - // torchEnabled: false,  
204 - returnImage: true,  
205 - ),  
206 - onDetect: (capture) {  
207 - final List<Barcode> barcodes = capture.barcodes;  
208 - final Uint8List? image = capture.image;  
209 - for (final barcode in barcodes) {  
210 - debugPrint('Barcode found! ${barcode.rawValue}');  
211 - }  
212 - if (image != null) {  
213 - showDialog(  
214 - context: context,  
215 - builder: (context) =>  
216 - Image(image: MemoryImage(image)),  
217 - );  
218 - Future.delayed(const Duration(seconds: 5), () {  
219 - Navigator.pop(context);  
220 - });  
221 - }  
222 - },  
223 - ),  
224 - );  
225 - } 149 +Finally, dispose of the the `MobileScannerController` when you are done with it.
  150 +
  151 +```dart
  152 +@override
  153 +Future<void> dispose() async {
  154 + // Stop listening to lifecycle changes.
  155 + WidgetsBinding.instance.removeObserver(this);
  156 + // Stop listening to the barcode events.
  157 + unawaited(_subscription?.cancel());
  158 + _subscription = null;
  159 + // Dispose the widget itself.
  160 + super.dispose();
  161 + // Finally, dispose of the controller.
  162 + await controller.dispose();
  163 +}
226 ``` 164 ```
227 165
228 -### BarcodeCapture  
229 -  
230 -The onDetect function returns a BarcodeCapture objects which contains the following items.  
231 -  
232 -| Property name | Type | Description |  
233 -|---------------|---------------|-----------------------------------|  
234 -| barcodes | List<Barcode> | A list with scanned barcodes. |  
235 -| image | Uint8List? | If enabled, an image of the scan. |  
236 -  
237 -You can use the following properties of the Barcode object.  
238 -  
239 -| Property name | Type | Description |  
240 -|---------------|----------------|-------------------------------------|  
241 -| format | BarcodeFormat | |  
242 -| rawBytes | Uint8List? | binary scan result |  
243 -| rawValue | String? | Value if barcode is in UTF-8 format |  
244 -| displayValue | String? | |  
245 -| type | BarcodeType | |  
246 -| calendarEvent | CalendarEvent? | |  
247 -| contactInfo | ContactInfo? | |  
248 -| driverLicense | DriverLicense? | |  
249 -| email | Email? | |  
250 -| geoPoint | GeoPoint? | |  
251 -| phone | Phone? | |  
252 -| sms | SMS? | |  
253 -| url | UrlBookmark? | |  
254 -| wifi | WiFi? | WiFi Access-Point details | 166 +To display the camera preview, pass the controller to a `MobileScanner` widget.
  167 +
  168 +See the examples for runnable examples of various usages,
  169 +such as the basic usage, applying a scan window, or retrieving images from the barcodes.
@@ -78,7 +78,7 @@ class MobileScanner( @@ -78,7 +78,7 @@ class MobileScanner(
78 scanner.process(inputImage) 78 scanner.process(inputImage)
79 .addOnSuccessListener { barcodes -> 79 .addOnSuccessListener { barcodes ->
80 if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { 80 if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
81 - val newScannedBarcodes = barcodes.mapNotNull({ barcode -> barcode.rawValue }).sorted() 81 + val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted()
82 if (newScannedBarcodes == lastScanned) { 82 if (newScannedBarcodes == lastScanned) {
83 // New scanned is duplicate, returning 83 // New scanned is duplicate, returning
84 return@addOnSuccessListener 84 return@addOnSuccessListener
@@ -424,7 +424,7 @@ class MobileScanner( @@ -424,7 +424,7 @@ class MobileScanner(
424 /** 424 /**
425 * Analyze a single image. 425 * Analyze a single image.
426 */ 426 */
427 - fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) { 427 + fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) {
428 val inputImage = InputImage.fromFilePath(activity, image) 428 val inputImage = InputImage.fromFilePath(activity, image)
429 429
430 scanner.process(inputImage) 430 scanner.process(inputImage)
@@ -432,15 +432,13 @@ class MobileScanner( @@ -432,15 +432,13 @@ class MobileScanner(
432 val barcodeMap = barcodes.map { barcode -> barcode.data } 432 val barcodeMap = barcodes.map { barcode -> barcode.data }
433 433
434 if (barcodeMap.isNotEmpty()) { 434 if (barcodeMap.isNotEmpty()) {
435 - analyzerCallback(barcodeMap) 435 + onSuccess(barcodeMap)
436 } else { 436 } else {
437 - analyzerCallback(null) 437 + onSuccess(null)
438 } 438 }
439 } 439 }
440 .addOnFailureListener { e -> 440 .addOnFailureListener { e ->
441 - mobileScannerErrorCallback(  
442 - e.localizedMessage ?: e.toString()  
443 - ) 441 + onError(e.localizedMessage ?: e.toString())
444 } 442 }
445 } 443 }
446 444
@@ -3,7 +3,8 @@ package dev.steenbakker.mobile_scanner @@ -3,7 +3,8 @@ package dev.steenbakker.mobile_scanner
3 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters 3 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
4 4
5 typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit 5 typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit
6 -typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit 6 +typealias AnalyzerErrorCallback = (message: String) -> Unit
  7 +typealias AnalyzerSuccessCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
7 typealias MobileScannerErrorCallback = (error: String) -> Unit 8 typealias MobileScannerErrorCallback = (error: String) -> Unit
8 typealias TorchStateCallback = (state: Int) -> Unit 9 typealias TorchStateCallback = (state: Int) -> Unit
9 typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit 10 typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit
@@ -26,16 +26,19 @@ class MobileScannerHandler( @@ -26,16 +26,19 @@ class MobileScannerHandler(
26 private val addPermissionListener: (RequestPermissionsResultListener) -> Unit, 26 private val addPermissionListener: (RequestPermissionsResultListener) -> Unit,
27 textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler { 27 textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler {
28 28
29 - private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->  
30 - if (barcodes != null) {  
31 - barcodeHandler.publishEvent(mapOf(  
32 - "name" to "barcode",  
33 - "data" to barcodes  
34 - )) 29 + private val analyzeImageErrorCallback: AnalyzerErrorCallback = {
  30 + Handler(Looper.getMainLooper()).post {
  31 + analyzerResult?.error("MobileScanner", it, null)
  32 + analyzerResult = null
  33 + }
35 } 34 }
36 35
  36 + private val analyzeImageSuccessCallback: AnalyzerSuccessCallback = {
37 Handler(Looper.getMainLooper()).post { 37 Handler(Looper.getMainLooper()).post {
38 - analyzerResult?.success(barcodes != null) 38 + analyzerResult?.success(mapOf(
  39 + "name" to "barcode",
  40 + "data" to it
  41 + ))
39 analyzerResult = null 42 analyzerResult = null
40 } 43 }
41 } 44 }
@@ -236,7 +239,8 @@ class MobileScannerHandler( @@ -236,7 +239,8 @@ class MobileScannerHandler(
236 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { 239 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
237 analyzerResult = result 240 analyzerResult = result
238 val uri = Uri.fromFile(File(call.arguments.toString())) 241 val uri = Uri.fromFile(File(call.arguments.toString()))
239 - mobileScanner!!.analyzeImage(uri, analyzerCallback) 242 +
  243 + mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback)
240 } 244 }
241 245
242 private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { 246 private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
@@ -265,7 +269,7 @@ class MobileScannerHandler( @@ -265,7 +269,7 @@ class MobileScannerHandler(
265 } 269 }
266 270
267 private fun updateScanWindow(call: MethodCall, result: MethodChannel.Result) { 271 private fun updateScanWindow(call: MethodCall, result: MethodChannel.Result) {
268 - mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect") 272 + mobileScanner?.scanWindow = call.argument<List<Float>?>("rect")
269 273
270 result.success(null) 274 result.success(null)
271 } 275 }
1 -import 'package:flutter/material.dart';  
2 -import 'package:image_picker/image_picker.dart';  
3 -import 'package:mobile_scanner/mobile_scanner.dart';  
4 -import 'package:mobile_scanner_example/scanner_error_widget.dart';  
5 -  
6 -class BarcodeListScannerWithController extends StatefulWidget {  
7 - const BarcodeListScannerWithController({super.key});  
8 -  
9 - @override  
10 - State<BarcodeListScannerWithController> createState() =>  
11 - _BarcodeListScannerWithControllerState();  
12 -}  
13 -  
14 -class _BarcodeListScannerWithControllerState  
15 - extends State<BarcodeListScannerWithController>  
16 - with SingleTickerProviderStateMixin {  
17 - BarcodeCapture? barcodeCapture;  
18 -  
19 - final MobileScannerController controller = MobileScannerController(  
20 - torchEnabled: true,  
21 - // formats: [BarcodeFormat.qrCode]  
22 - // facing: CameraFacing.front,  
23 - // detectionSpeed: DetectionSpeed.normal  
24 - // detectionTimeoutMs: 1000,  
25 - // returnImage: false,  
26 - );  
27 -  
28 - bool isStarted = true;  
29 -  
30 - void _startOrStop() {  
31 - try {  
32 - if (isStarted) {  
33 - controller.stop();  
34 - } else {  
35 - controller.start();  
36 - }  
37 - setState(() {  
38 - isStarted = !isStarted;  
39 - });  
40 - } on Exception catch (e) {  
41 - ScaffoldMessenger.of(context).showSnackBar(  
42 - SnackBar(  
43 - content: Text('Something went wrong! $e'),  
44 - backgroundColor: Colors.red,  
45 - ),  
46 - );  
47 - }  
48 - }  
49 -  
50 - @override  
51 - Widget build(BuildContext context) {  
52 - return Scaffold(  
53 - appBar: AppBar(title: const Text('With ValueListenableBuilder')),  
54 - backgroundColor: Colors.black,  
55 - body: Builder(  
56 - builder: (context) {  
57 - return Stack(  
58 - children: [  
59 - MobileScanner(  
60 - controller: controller,  
61 - errorBuilder: (context, error, child) {  
62 - return ScannerErrorWidget(error: error);  
63 - },  
64 - fit: BoxFit.contain,  
65 - onDetect: (barcodeCapture) {  
66 - setState(() {  
67 - this.barcodeCapture = barcodeCapture;  
68 - });  
69 - },  
70 - onScannerStarted: (arguments) {  
71 - // Do something with arguments.  
72 - },  
73 - ),  
74 - Align(  
75 - alignment: Alignment.bottomCenter,  
76 - child: Container(  
77 - alignment: Alignment.bottomCenter,  
78 - height: 100,  
79 - color: Colors.black.withOpacity(0.4),  
80 - child: Row(  
81 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
82 - children: [  
83 - IconButton(  
84 - color: Colors.white,  
85 - icon: ValueListenableBuilder<TorchState>(  
86 - valueListenable: controller.torchState,  
87 - builder: (context, state, child) {  
88 - switch (state) {  
89 - case TorchState.off:  
90 - return const Icon(  
91 - Icons.flash_off,  
92 - color: Colors.grey,  
93 - );  
94 - case TorchState.on:  
95 - return const Icon(  
96 - Icons.flash_on,  
97 - color: Colors.yellow,  
98 - );  
99 - }  
100 - },  
101 - ),  
102 - iconSize: 32.0,  
103 - onPressed: () => controller.toggleTorch(),  
104 - ),  
105 - IconButton(  
106 - color: Colors.white,  
107 - icon: isStarted  
108 - ? const Icon(Icons.stop)  
109 - : const Icon(Icons.play_arrow),  
110 - iconSize: 32.0,  
111 - onPressed: _startOrStop,  
112 - ),  
113 - Center(  
114 - child: SizedBox(  
115 - width: MediaQuery.of(context).size.width - 200,  
116 - height: 50,  
117 - child: FittedBox(  
118 - child: Text(  
119 - '${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}',  
120 - overflow: TextOverflow.fade,  
121 - style: Theme.of(context)  
122 - .textTheme  
123 - .headlineMedium!  
124 - .copyWith(color: Colors.white),  
125 - ),  
126 - ),  
127 - ),  
128 - ),  
129 - IconButton(  
130 - color: Colors.white,  
131 - icon: ValueListenableBuilder<CameraFacing>(  
132 - valueListenable: controller.cameraFacingState,  
133 - builder: (context, state, child) {  
134 - switch (state) {  
135 - case CameraFacing.front:  
136 - return const Icon(Icons.camera_front);  
137 - case CameraFacing.back:  
138 - return const Icon(Icons.camera_rear);  
139 - }  
140 - },  
141 - ),  
142 - iconSize: 32.0,  
143 - onPressed: () => controller.switchCamera(),  
144 - ),  
145 - IconButton(  
146 - color: Colors.white,  
147 - icon: const Icon(Icons.image),  
148 - iconSize: 32.0,  
149 - onPressed: () async {  
150 - final ImagePicker picker = ImagePicker();  
151 - // Pick an image  
152 - final XFile? image = await picker.pickImage(  
153 - source: ImageSource.gallery,  
154 - );  
155 - if (image != null) {  
156 - if (await controller.analyzeImage(image.path)) {  
157 - if (!context.mounted) return;  
158 - ScaffoldMessenger.of(context).showSnackBar(  
159 - const SnackBar(  
160 - content: Text('Barcode found!'),  
161 - backgroundColor: Colors.green,  
162 - ),  
163 - );  
164 - } else {  
165 - if (!context.mounted) return;  
166 - ScaffoldMessenger.of(context).showSnackBar(  
167 - const SnackBar(  
168 - content: Text('No barcode found!'),  
169 - backgroundColor: Colors.red,  
170 - ),  
171 - );  
172 - }  
173 - }  
174 - },  
175 - ),  
176 - ],  
177 - ),  
178 - ),  
179 - ),  
180 - ],  
181 - );  
182 - },  
183 - ),  
184 - );  
185 - }  
186 -  
187 - @override  
188 - void dispose() {  
189 - controller.dispose();  
190 - super.dispose();  
191 - }  
192 -}  
  1 +import 'dart:async';
  2 +
1 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
2 -import 'package:image_picker/image_picker.dart';  
3 import 'package:mobile_scanner/mobile_scanner.dart'; 4 import 'package:mobile_scanner/mobile_scanner.dart';
  5 +import 'package:mobile_scanner_example/scanner_button_widgets.dart';
4 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 6 import 'package:mobile_scanner_example/scanner_error_widget.dart';
5 7
6 class BarcodeScannerWithController extends StatefulWidget { 8 class BarcodeScannerWithController extends StatefulWidget {
@@ -12,10 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget { @@ -12,10 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget {
12 } 14 }
13 15
14 class _BarcodeScannerWithControllerState 16 class _BarcodeScannerWithControllerState
15 - extends State<BarcodeScannerWithController>  
16 - with SingleTickerProviderStateMixin {  
17 - BarcodeCapture? barcode;  
18 - 17 + extends State<BarcodeScannerWithController> with WidgetsBindingObserver {
19 final MobileScannerController controller = MobileScannerController( 18 final MobileScannerController controller = MobileScannerController(
20 torchEnabled: true, useNewCameraSelector: true, 19 torchEnabled: true, useNewCameraSelector: true,
21 // formats: [BarcodeFormat.qrCode] 20 // formats: [BarcodeFormat.qrCode]
@@ -25,56 +24,76 @@ class _BarcodeScannerWithControllerState @@ -25,56 +24,76 @@ class _BarcodeScannerWithControllerState
25 // returnImage: false, 24 // returnImage: false,
26 ); 25 );
27 26
28 - bool isStarted = true; 27 + Barcode? _barcode;
  28 + StreamSubscription<Object?>? _subscription;
  29 +
  30 + Widget _buildBarcode(Barcode? value) {
  31 + if (value == null) {
  32 + return const Text(
  33 + 'Scan something!',
  34 + overflow: TextOverflow.fade,
  35 + style: TextStyle(color: Colors.white),
  36 + );
  37 + }
29 38
30 - void _startOrStop() {  
31 - try {  
32 - if (isStarted) {  
33 - controller.stop();  
34 - } else {  
35 - controller.start(); 39 + return Text(
  40 + value.displayValue ?? 'No display value.',
  41 + overflow: TextOverflow.fade,
  42 + style: const TextStyle(color: Colors.white),
  43 + );
36 } 44 }
  45 +
  46 + void _handleBarcode(BarcodeCapture barcodes) {
  47 + if (mounted) {
37 setState(() { 48 setState(() {
38 - isStarted = !isStarted; 49 + _barcode = barcodes.barcodes.firstOrNull;
39 }); 50 });
40 - } on Exception catch (e) {  
41 - ScaffoldMessenger.of(context).showSnackBar(  
42 - SnackBar(  
43 - content: Text('Something went wrong! $e'),  
44 - backgroundColor: Colors.red,  
45 - ),  
46 - );  
47 } 51 }
48 } 52 }
49 53
50 - int? numberOfCameras; 54 + @override
  55 + void initState() {
  56 + super.initState();
  57 + WidgetsBinding.instance.addObserver(this);
  58 +
  59 + _subscription = controller.barcodes.listen(_handleBarcode);
  60 +
  61 + unawaited(controller.start());
  62 + }
  63 +
  64 + @override
  65 + void didChangeAppLifecycleState(AppLifecycleState state) {
  66 + super.didChangeAppLifecycleState(state);
  67 +
  68 + switch (state) {
  69 + case AppLifecycleState.detached:
  70 + case AppLifecycleState.hidden:
  71 + case AppLifecycleState.paused:
  72 + return;
  73 + case AppLifecycleState.resumed:
  74 + _subscription = controller.barcodes.listen(_handleBarcode);
  75 +
  76 + unawaited(controller.start());
  77 + case AppLifecycleState.inactive:
  78 + unawaited(_subscription?.cancel());
  79 + _subscription = null;
  80 + unawaited(controller.stop());
  81 + }
  82 + }
51 83
52 @override 84 @override
53 Widget build(BuildContext context) { 85 Widget build(BuildContext context) {
54 return Scaffold( 86 return Scaffold(
55 appBar: AppBar(title: const Text('With controller')), 87 appBar: AppBar(title: const Text('With controller')),
56 backgroundColor: Colors.black, 88 backgroundColor: Colors.black,
57 - body: Builder(  
58 - builder: (context) {  
59 - return Stack( 89 + body: Stack(
60 children: [ 90 children: [
61 MobileScanner( 91 MobileScanner(
62 - onScannerStarted: (arguments) {  
63 - if (mounted && arguments?.numberOfCameras != null) {  
64 - numberOfCameras = arguments!.numberOfCameras;  
65 - setState(() {});  
66 - }  
67 - },  
68 controller: controller, 92 controller: controller,
69 errorBuilder: (context, error, child) { 93 errorBuilder: (context, error, child) {
70 return ScannerErrorWidget(error: error); 94 return ScannerErrorWidget(error: error);
71 }, 95 },
72 fit: BoxFit.contain, 96 fit: BoxFit.contain,
73 - onDetect: (barcode) {  
74 - setState(() {  
75 - this.barcode = barcode;  
76 - });  
77 - },  
78 ), 97 ),
79 Align( 98 Align(
80 alignment: Alignment.bottomCenter, 99 alignment: Alignment.bottomCenter,
@@ -85,118 +104,26 @@ class _BarcodeScannerWithControllerState @@ -85,118 +104,26 @@ class _BarcodeScannerWithControllerState
85 child: Row( 104 child: Row(
86 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 105 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
87 children: [ 106 children: [
88 - ValueListenableBuilder(  
89 - valueListenable: controller.hasTorchState,  
90 - builder: (context, state, child) {  
91 - if (state != true) {  
92 - return const SizedBox.shrink();  
93 - }  
94 - return IconButton(  
95 - color: Colors.white,  
96 - icon: ValueListenableBuilder<TorchState>(  
97 - valueListenable: controller.torchState,  
98 - builder: (context, state, child) {  
99 - switch (state) {  
100 - case TorchState.off:  
101 - return const Icon(  
102 - Icons.flash_off,  
103 - color: Colors.grey,  
104 - );  
105 - case TorchState.on:  
106 - return const Icon(  
107 - Icons.flash_on,  
108 - color: Colors.yellow,  
109 - );  
110 - }  
111 - },  
112 - ),  
113 - iconSize: 32.0,  
114 - onPressed: () => controller.toggleTorch(),  
115 - );  
116 - },  
117 - ),  
118 - IconButton(  
119 - color: Colors.white,  
120 - icon: isStarted  
121 - ? const Icon(Icons.stop)  
122 - : const Icon(Icons.play_arrow),  
123 - iconSize: 32.0,  
124 - onPressed: _startOrStop,  
125 - ),  
126 - Center(  
127 - child: SizedBox(  
128 - width: MediaQuery.of(context).size.width - 200,  
129 - height: 50,  
130 - child: FittedBox(  
131 - child: Text(  
132 - barcode?.barcodes.first.rawValue ??  
133 - 'Scan something!',  
134 - overflow: TextOverflow.fade,  
135 - style: Theme.of(context)  
136 - .textTheme  
137 - .headlineMedium!  
138 - .copyWith(color: Colors.white),  
139 - ),  
140 - ),  
141 - ),  
142 - ),  
143 - IconButton(  
144 - color: Colors.white,  
145 - icon: ValueListenableBuilder<CameraFacing>(  
146 - valueListenable: controller.cameraFacingState,  
147 - builder: (context, state, child) {  
148 - switch (state) {  
149 - case CameraFacing.front:  
150 - return const Icon(Icons.camera_front);  
151 - case CameraFacing.back:  
152 - return const Icon(Icons.camera_rear);  
153 - }  
154 - },  
155 - ),  
156 - iconSize: 32.0,  
157 - onPressed: (numberOfCameras ?? 0) < 2  
158 - ? null  
159 - : () => controller.switchCamera(),  
160 - ),  
161 - IconButton(  
162 - color: Colors.white,  
163 - icon: const Icon(Icons.image),  
164 - iconSize: 32.0,  
165 - onPressed: () async {  
166 - final ImagePicker picker = ImagePicker();  
167 - // Pick an image  
168 - final XFile? image = await picker.pickImage(  
169 - source: ImageSource.gallery,  
170 - );  
171 - if (image != null) {  
172 - if (await controller.analyzeImage(image.path)) {  
173 - if (!context.mounted) return;  
174 - ScaffoldMessenger.of(context).showSnackBar(  
175 - const SnackBar(  
176 - content: Text('Barcode found!'),  
177 - backgroundColor: Colors.green,  
178 - ),  
179 - );  
180 - } else {  
181 - if (!context.mounted) return;  
182 - ScaffoldMessenger.of(context).showSnackBar(  
183 - const SnackBar(  
184 - content: Text('No barcode found!'),  
185 - backgroundColor: Colors.red,  
186 - ),  
187 - );  
188 - }  
189 - }  
190 - },  
191 - ), 107 + ToggleFlashlightButton(controller: controller),
  108 + StartStopMobileScannerButton(controller: controller),
  109 + Expanded(child: Center(child: _buildBarcode(_barcode))),
  110 + SwitchCameraButton(controller: controller),
  111 + AnalyzeImageFromGalleryButton(controller: controller),
192 ], 112 ],
193 ), 113 ),
194 ), 114 ),
195 ), 115 ),
196 ], 116 ],
197 - );  
198 - },  
199 ), 117 ),
200 ); 118 );
201 } 119 }
  120 +
  121 + @override
  122 + Future<void> dispose() async {
  123 + WidgetsBinding.instance.removeObserver(this);
  124 + unawaited(_subscription?.cancel());
  125 + _subscription = null;
  126 + super.dispose();
  127 + await controller.dispose();
  128 + }
202 } 129 }
  1 +import 'dart:async';
  2 +
  3 +import 'package:flutter/material.dart';
  4 +import 'package:mobile_scanner/mobile_scanner.dart';
  5 +import 'package:mobile_scanner_example/scanner_button_widgets.dart';
  6 +import 'package:mobile_scanner_example/scanner_error_widget.dart';
  7 +
  8 +class BarcodeScannerListView extends StatefulWidget {
  9 + const BarcodeScannerListView({super.key});
  10 +
  11 + @override
  12 + State<BarcodeScannerListView> createState() => _BarcodeScannerListViewState();
  13 +}
  14 +
  15 +class _BarcodeScannerListViewState extends State<BarcodeScannerListView> {
  16 + final MobileScannerController controller = MobileScannerController(
  17 + torchEnabled: true,
  18 + // formats: [BarcodeFormat.qrCode]
  19 + // facing: CameraFacing.front,
  20 + // detectionSpeed: DetectionSpeed.normal
  21 + // detectionTimeoutMs: 1000,
  22 + // returnImage: false,
  23 + );
  24 +
  25 + @override
  26 + void initState() {
  27 + super.initState();
  28 +
  29 + controller.start();
  30 + }
  31 +
  32 + Widget _buildBarcodesListView() {
  33 + return StreamBuilder<BarcodeCapture>(
  34 + stream: controller.barcodes,
  35 + builder: (context, snapshot) {
  36 + final barcodes = snapshot.data?.barcodes;
  37 +
  38 + if (barcodes == null || barcodes.isEmpty) {
  39 + return const Center(
  40 + child: Text(
  41 + 'Scan Something!',
  42 + style: TextStyle(color: Colors.white, fontSize: 20),
  43 + ),
  44 + );
  45 + }
  46 +
  47 + return ListView.builder(
  48 + itemCount: barcodes.length,
  49 + itemBuilder: (context, index) {
  50 + return Padding(
  51 + padding: const EdgeInsets.all(8.0),
  52 + child: Text(
  53 + barcodes[index].rawValue ?? 'No raw value',
  54 + overflow: TextOverflow.fade,
  55 + style: const TextStyle(color: Colors.white),
  56 + ),
  57 + );
  58 + },
  59 + );
  60 + },
  61 + );
  62 + }
  63 +
  64 + @override
  65 + Widget build(BuildContext context) {
  66 + return Scaffold(
  67 + appBar: AppBar(title: const Text('With ListView')),
  68 + backgroundColor: Colors.black,
  69 + body: Stack(
  70 + children: [
  71 + MobileScanner(
  72 + controller: controller,
  73 + errorBuilder: (context, error, child) {
  74 + return ScannerErrorWidget(error: error);
  75 + },
  76 + fit: BoxFit.contain,
  77 + ),
  78 + Align(
  79 + alignment: Alignment.bottomCenter,
  80 + child: Container(
  81 + alignment: Alignment.bottomCenter,
  82 + height: 100,
  83 + color: Colors.black.withOpacity(0.4),
  84 + child: Column(
  85 + children: [
  86 + Expanded(
  87 + child: _buildBarcodesListView(),
  88 + ),
  89 + Row(
  90 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  91 + children: [
  92 + ToggleFlashlightButton(controller: controller),
  93 + StartStopMobileScannerButton(controller: controller),
  94 + const Spacer(),
  95 + SwitchCameraButton(controller: controller),
  96 + AnalyzeImageFromGalleryButton(controller: controller),
  97 + ],
  98 + ),
  99 + ],
  100 + ),
  101 + ),
  102 + ),
  103 + ],
  104 + ),
  105 + );
  106 + }
  107 +
  108 + @override
  109 + Future<void> dispose() async {
  110 + super.dispose();
  111 + await controller.dispose();
  112 + }
  113 +}
  1 +import 'dart:async';
  2 +
1 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
2 import 'package:mobile_scanner/mobile_scanner.dart'; 4 import 'package:mobile_scanner/mobile_scanner.dart';
  5 +import 'package:mobile_scanner_example/scanned_barcode_label.dart';
3 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 6 import 'package:mobile_scanner_example/scanner_error_widget.dart';
4 7
5 class BarcodeScannerPageView extends StatefulWidget { 8 class BarcodeScannerPageView extends StatefulWidget {
@@ -9,27 +12,72 @@ class BarcodeScannerPageView extends StatefulWidget { @@ -9,27 +12,72 @@ class BarcodeScannerPageView extends StatefulWidget {
9 State<BarcodeScannerPageView> createState() => _BarcodeScannerPageViewState(); 12 State<BarcodeScannerPageView> createState() => _BarcodeScannerPageViewState();
10 } 13 }
11 14
12 -class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView>  
13 - with SingleTickerProviderStateMixin {  
14 - BarcodeCapture? capture; 15 +class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> {
  16 + final MobileScannerController controller = MobileScannerController();
  17 +
  18 + final PageController pageController = PageController();
  19 +
  20 + @override
  21 + void initState() {
  22 + super.initState();
  23 + unawaited(controller.start());
  24 + }
  25 +
  26 + @override
  27 + Widget build(BuildContext context) {
  28 + return Scaffold(
  29 + appBar: AppBar(title: const Text('With PageView')),
  30 + backgroundColor: Colors.black,
  31 + body: PageView(
  32 + controller: pageController,
  33 + onPageChanged: (index) async {
  34 + // Stop the camera view for the current page,
  35 + // and then restart the camera for the new page.
  36 + await controller.stop();
  37 +
  38 + // When switching pages, add a delay to the next start call.
  39 + // Otherwise the camera will start before the next page is displayed.
  40 + await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
  41 +
  42 + if (!mounted) {
  43 + return;
  44 + }
  45 +
  46 + unawaited(controller.start());
  47 + },
  48 + children: [
  49 + _BarcodeScannerPage(controller: controller),
  50 + const SizedBox(),
  51 + _BarcodeScannerPage(controller: controller),
  52 + _BarcodeScannerPage(controller: controller),
  53 + ],
  54 + ),
  55 + );
  56 + }
  57 +
  58 + @override
  59 + Future<void> dispose() async {
  60 + pageController.dispose();
  61 + super.dispose();
  62 + await controller.dispose();
  63 + }
  64 +}
15 65
16 - Widget cameraView() {  
17 - return Builder(  
18 - builder: (context) { 66 +class _BarcodeScannerPage extends StatelessWidget {
  67 + const _BarcodeScannerPage({required this.controller});
  68 +
  69 + final MobileScannerController controller;
  70 +
  71 + @override
  72 + Widget build(BuildContext context) {
19 return Stack( 73 return Stack(
20 children: [ 74 children: [
21 MobileScanner( 75 MobileScanner(
22 - startDelay: true,  
23 - controller: MobileScannerController(torchEnabled: true), 76 + controller: controller,
24 fit: BoxFit.contain, 77 fit: BoxFit.contain,
25 errorBuilder: (context, error, child) { 78 errorBuilder: (context, error, child) {
26 return ScannerErrorWidget(error: error); 79 return ScannerErrorWidget(error: error);
27 }, 80 },
28 - onDetect: (capture) {  
29 - setState(() {  
30 - this.capture = capture;  
31 - });  
32 - },  
33 ), 81 ),
34 Align( 82 Align(
35 alignment: Alignment.bottomCenter, 83 alignment: Alignment.bottomCenter,
@@ -37,49 +85,12 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> @@ -37,49 +85,12 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView>
37 alignment: Alignment.bottomCenter, 85 alignment: Alignment.bottomCenter,
38 height: 100, 86 height: 100,
39 color: Colors.black.withOpacity(0.4), 87 color: Colors.black.withOpacity(0.4),
40 - child: Row(  
41 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
42 - children: [  
43 - Center(  
44 - child: SizedBox(  
45 - width: MediaQuery.of(context).size.width - 120,  
46 - height: 50,  
47 - child: FittedBox(  
48 - child: Text(  
49 - capture?.barcodes.first.rawValue ??  
50 - 'Scan something!',  
51 - overflow: TextOverflow.fade,  
52 - style: Theme.of(context)  
53 - .textTheme  
54 - .headlineMedium!  
55 - .copyWith(color: Colors.white),  
56 - ), 88 + child: Center(
  89 + child: ScannedBarcodeLabel(barcodes: controller.barcodes),
57 ), 90 ),
58 ), 91 ),
59 ), 92 ),
60 ], 93 ],
61 - ),  
62 - ),  
63 - ),  
64 - ],  
65 - );  
66 - },  
67 - );  
68 - }  
69 -  
70 - @override  
71 - Widget build(BuildContext context) {  
72 - return Scaffold(  
73 - appBar: AppBar(title: const Text('With PageView')),  
74 - backgroundColor: Colors.black,  
75 - body: PageView(  
76 - children: [  
77 - cameraView(),  
78 - Container(),  
79 - cameraView(),  
80 - cameraView(),  
81 - ],  
82 - ),  
83 ); 94 );
84 } 95 }
85 } 96 }
@@ -2,6 +2,8 @@ import 'dart:math'; @@ -2,6 +2,8 @@ import 'dart:math';
2 2
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
4 import 'package:mobile_scanner/mobile_scanner.dart'; 4 import 'package:mobile_scanner/mobile_scanner.dart';
  5 +import 'package:mobile_scanner_example/scanned_barcode_label.dart';
  6 +import 'package:mobile_scanner_example/scanner_button_widgets.dart';
5 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 7 import 'package:mobile_scanner_example/scanner_error_widget.dart';
6 8
7 class BarcodeScannerReturningImage extends StatefulWidget { 9 class BarcodeScannerReturningImage extends StatefulWidget {
@@ -13,11 +15,7 @@ class BarcodeScannerReturningImage extends StatefulWidget { @@ -13,11 +15,7 @@ class BarcodeScannerReturningImage extends StatefulWidget {
13 } 15 }
14 16
15 class _BarcodeScannerReturningImageState 17 class _BarcodeScannerReturningImageState
16 - extends State<BarcodeScannerReturningImage>  
17 - with SingleTickerProviderStateMixin {  
18 - BarcodeCapture? barcode;  
19 - // MobileScannerArguments? arguments;  
20 - 18 + extends State<BarcodeScannerReturningImage> {
21 final MobileScannerController controller = MobileScannerController( 19 final MobileScannerController controller = MobileScannerController(
22 torchEnabled: true, 20 torchEnabled: true,
23 // formats: [BarcodeFormat.qrCode] 21 // formats: [BarcodeFormat.qrCode]
@@ -27,27 +25,11 @@ class _BarcodeScannerReturningImageState @@ -27,27 +25,11 @@ class _BarcodeScannerReturningImageState
27 returnImage: true, 25 returnImage: true,
28 ); 26 );
29 27
30 - bool isStarted = true;  
31 -  
32 - void _startOrStop() {  
33 - try {  
34 - if (isStarted) {  
35 - controller.stop();  
36 - } else { 28 + @override
  29 + void initState() {
  30 + super.initState();
37 controller.start(); 31 controller.start();
38 } 32 }
39 - setState(() {  
40 - isStarted = !isStarted;  
41 - });  
42 - } on Exception catch (e) {  
43 - ScaffoldMessenger.of(context).showSnackBar(  
44 - SnackBar(  
45 - content: Text('Something went wrong! $e'),  
46 - backgroundColor: Colors.red,  
47 - ),  
48 - );  
49 - }  
50 - }  
51 33
52 @override 34 @override
53 Widget build(BuildContext context) { 35 Widget build(BuildContext context) {
@@ -57,19 +39,54 @@ class _BarcodeScannerReturningImageState @@ -57,19 +39,54 @@ class _BarcodeScannerReturningImageState
57 child: Column( 39 child: Column(
58 children: [ 40 children: [
59 Expanded( 41 Expanded(
60 - child: barcode?.image != null  
61 - ? Transform.rotate(  
62 - angle: 90 * pi / 180,  
63 - child: Image(  
64 - gaplessPlayback: true,  
65 - image: MemoryImage(barcode!.image!),  
66 - fit: BoxFit.contain,  
67 - ),  
68 - )  
69 - : const Center( 42 + child: StreamBuilder<BarcodeCapture>(
  43 + stream: controller.barcodes,
  44 + builder: (context, snapshot) {
  45 + final barcode = snapshot.data;
  46 +
  47 + if (barcode == null) {
  48 + return const Center(
70 child: Text( 49 child: Text(
71 'Your scanned barcode will appear here!', 50 'Your scanned barcode will appear here!',
72 ), 51 ),
  52 + );
  53 + }
  54 +
  55 + final barcodeImage = barcode.image;
  56 +
  57 + if (barcodeImage == null) {
  58 + return const Center(
  59 + child: Text('No image for this barcode.'),
  60 + );
  61 + }
  62 +
  63 + return Image.memory(
  64 + barcodeImage,
  65 + fit: BoxFit.contain,
  66 + errorBuilder: (context, error, stackTrace) {
  67 + return Center(
  68 + child: Text('Could not decode image bytes. $error'),
  69 + );
  70 + },
  71 + frameBuilder: (
  72 + BuildContext context,
  73 + Widget child,
  74 + int? frame,
  75 + bool? wasSynchronouslyLoaded,
  76 + ) {
  77 + if (wasSynchronouslyLoaded == true || frame != null) {
  78 + return Transform.rotate(
  79 + angle: 90 * pi / 180,
  80 + child: child,
  81 + );
  82 + }
  83 +
  84 + return const Center(
  85 + child: CircularProgressIndicator(),
  86 + );
  87 + },
  88 + );
  89 + },
73 ), 90 ),
74 ), 91 ),
75 Expanded( 92 Expanded(
@@ -84,11 +101,6 @@ class _BarcodeScannerReturningImageState @@ -84,11 +101,6 @@ class _BarcodeScannerReturningImageState
84 return ScannerErrorWidget(error: error); 101 return ScannerErrorWidget(error: error);
85 }, 102 },
86 fit: BoxFit.contain, 103 fit: BoxFit.contain,
87 - onDetect: (barcode) {  
88 - setState(() {  
89 - this.barcode = barcode;  
90 - });  
91 - },  
92 ), 104 ),
93 Align( 105 Align(
94 alignment: Alignment.bottomCenter, 106 alignment: Alignment.bottomCenter,
@@ -99,69 +111,18 @@ class _BarcodeScannerReturningImageState @@ -99,69 +111,18 @@ class _BarcodeScannerReturningImageState
99 child: Row( 111 child: Row(
100 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 112 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
101 children: [ 113 children: [
102 - IconButton(  
103 - color: Colors.white,  
104 - icon: ValueListenableBuilder<TorchState>(  
105 - valueListenable: controller.torchState,  
106 - builder: (context, state, child) {  
107 - switch (state) {  
108 - case TorchState.off:  
109 - return const Icon(  
110 - Icons.flash_off,  
111 - color: Colors.grey,  
112 - );  
113 - case TorchState.on:  
114 - return const Icon(  
115 - Icons.flash_on,  
116 - color: Colors.yellow,  
117 - );  
118 - }  
119 - }, 114 + ToggleFlashlightButton(controller: controller),
  115 + StartStopMobileScannerButton(
  116 + controller: controller,
  117 + ),
  118 + Expanded(
  119 + child: Center(
  120 + child: ScannedBarcodeLabel(
  121 + barcodes: controller.barcodes,
120 ), 122 ),
121 - iconSize: 32.0,  
122 - onPressed: () => controller.toggleTorch(),  
123 - ),  
124 - IconButton(  
125 - color: Colors.white,  
126 - icon: isStarted  
127 - ? const Icon(Icons.stop)  
128 - : const Icon(Icons.play_arrow),  
129 - iconSize: 32.0,  
130 - onPressed: _startOrStop,  
131 - ),  
132 - Center(  
133 - child: SizedBox(  
134 - width: MediaQuery.of(context).size.width - 200,  
135 - height: 50,  
136 - child: FittedBox(  
137 - child: Text(  
138 - barcode?.barcodes.first.rawValue ??  
139 - 'Scan something!',  
140 - overflow: TextOverflow.fade,  
141 - style: Theme.of(context)  
142 - .textTheme  
143 - .headlineMedium!  
144 - .copyWith(color: Colors.white),  
145 - ),  
146 - ),  
147 - ),  
148 - ),  
149 - IconButton(  
150 - color: Colors.white,  
151 - icon: ValueListenableBuilder<CameraFacing>(  
152 - valueListenable: controller.cameraFacingState,  
153 - builder: (context, state, child) {  
154 - switch (state) {  
155 - case CameraFacing.front:  
156 - return const Icon(Icons.camera_front);  
157 - case CameraFacing.back:  
158 - return const Icon(Icons.camera_rear);  
159 - }  
160 - },  
161 ), 123 ),
162 - iconSize: 32.0,  
163 - onPressed: () => controller.switchCamera(),  
164 ), 124 ),
  125 + SwitchCameraButton(controller: controller),
165 ], 126 ],
166 ), 127 ),
167 ), 128 ),
@@ -177,8 +138,8 @@ class _BarcodeScannerReturningImageState @@ -177,8 +138,8 @@ class _BarcodeScannerReturningImageState
177 } 138 }
178 139
179 @override 140 @override
180 - void dispose() {  
181 - controller.dispose(); 141 + Future<void> dispose() async {
182 super.dispose(); 142 super.dispose();
  143 + await controller.dispose();
183 } 144 }
184 } 145 }
@@ -3,6 +3,7 @@ import 'dart:io'; @@ -3,6 +3,7 @@ import 'dart:io';
3 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
4 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
5 import 'package:mobile_scanner/mobile_scanner.dart'; 5 import 'package:mobile_scanner/mobile_scanner.dart';
  6 +import 'package:mobile_scanner_example/scanned_barcode_label.dart';
6 7
7 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 8 import 'package:mobile_scanner_example/scanner_error_widget.dart';
8 9
@@ -16,95 +17,120 @@ class BarcodeScannerWithScanWindow extends StatefulWidget { @@ -16,95 +17,120 @@ class BarcodeScannerWithScanWindow extends StatefulWidget {
16 17
17 class _BarcodeScannerWithScanWindowState 18 class _BarcodeScannerWithScanWindowState
18 extends State<BarcodeScannerWithScanWindow> { 19 extends State<BarcodeScannerWithScanWindow> {
19 - late MobileScannerController controller = MobileScannerController();  
20 - Barcode? barcode;  
21 - BarcodeCapture? capture; 20 + final MobileScannerController controller = MobileScannerController();
22 21
23 - Future<void> onDetect(BarcodeCapture barcode) async {  
24 - capture = barcode;  
25 - setState(() => this.barcode = barcode.barcodes.first); 22 + @override
  23 + void initState() {
  24 + super.initState();
  25 +
  26 + controller.start();
  27 + }
  28 +
  29 + Widget _buildBarcodeOverlay() {
  30 + return ValueListenableBuilder(
  31 + valueListenable: controller,
  32 + builder: (context, value, child) {
  33 + // Not ready.
  34 + if (!value.isInitialized || !value.isRunning || value.error != null) {
  35 + return const SizedBox();
  36 + }
  37 +
  38 + return StreamBuilder<BarcodeCapture>(
  39 + stream: controller.barcodes,
  40 + builder: (context, snapshot) {
  41 + final BarcodeCapture? barcodeCapture = snapshot.data;
  42 +
  43 + // No barcode.
  44 + if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) {
  45 + return const SizedBox();
26 } 46 }
27 47
28 - MobileScannerArguments? arguments; 48 + final scannedBarcode = barcodeCapture.barcodes.first;
  49 +
  50 + // No barcode corners, or size, or no camera preview size.
  51 + if (scannedBarcode.corners.isEmpty ||
  52 + value.size.isEmpty ||
  53 + barcodeCapture.size.isEmpty) {
  54 + return const SizedBox();
  55 + }
  56 +
  57 + return CustomPaint(
  58 + painter: BarcodeOverlay(
  59 + barcodeCorners: scannedBarcode.corners,
  60 + barcodeSize: barcodeCapture.size,
  61 + boxFit: BoxFit.contain,
  62 + cameraPreviewSize: value.size,
  63 + ),
  64 + );
  65 + },
  66 + );
  67 + },
  68 + );
  69 + }
  70 +
  71 + Widget _buildScanWindow(Rect scanWindowRect) {
  72 + return ValueListenableBuilder(
  73 + valueListenable: controller,
  74 + builder: (context, value, child) {
  75 + // Not ready.
  76 + if (!value.isInitialized ||
  77 + !value.isRunning ||
  78 + value.error != null ||
  79 + value.size.isEmpty) {
  80 + return const SizedBox();
  81 + }
  82 +
  83 + return CustomPaint(
  84 + painter: ScannerOverlay(scanWindowRect),
  85 + );
  86 + },
  87 + );
  88 + }
29 89
30 @override 90 @override
31 Widget build(BuildContext context) { 91 Widget build(BuildContext context) {
32 final scanWindow = Rect.fromCenter( 92 final scanWindow = Rect.fromCenter(
33 - center: MediaQuery.of(context).size.center(Offset.zero), 93 + center: MediaQuery.sizeOf(context).center(Offset.zero),
34 width: 200, 94 width: 200,
35 height: 200, 95 height: 200,
36 ); 96 );
  97 +
37 return Scaffold( 98 return Scaffold(
38 appBar: AppBar(title: const Text('With Scan window')), 99 appBar: AppBar(title: const Text('With Scan window')),
39 backgroundColor: Colors.black, 100 backgroundColor: Colors.black,
40 - body: Builder(  
41 - builder: (context) {  
42 - return Stack( 101 + body: Stack(
43 fit: StackFit.expand, 102 fit: StackFit.expand,
44 children: [ 103 children: [
45 MobileScanner( 104 MobileScanner(
46 fit: BoxFit.contain, 105 fit: BoxFit.contain,
47 scanWindow: scanWindow, 106 scanWindow: scanWindow,
48 controller: controller, 107 controller: controller,
49 - onScannerStarted: (arguments) {  
50 - setState(() {  
51 - this.arguments = arguments;  
52 - });  
53 - },  
54 errorBuilder: (context, error, child) { 108 errorBuilder: (context, error, child) {
55 return ScannerErrorWidget(error: error); 109 return ScannerErrorWidget(error: error);
56 }, 110 },
57 - onDetect: onDetect,  
58 - ),  
59 - if (barcode != null &&  
60 - barcode?.corners != null &&  
61 - arguments != null)  
62 - CustomPaint(  
63 - painter: BarcodeOverlay(  
64 - barcode: barcode!,  
65 - arguments: arguments!,  
66 - boxFit: BoxFit.contain,  
67 - capture: capture!,  
68 - ),  
69 - ),  
70 - CustomPaint(  
71 - painter: ScannerOverlay(scanWindow),  
72 ), 111 ),
  112 + _buildBarcodeOverlay(),
  113 + _buildScanWindow(scanWindow),
73 Align( 114 Align(
74 alignment: Alignment.bottomCenter, 115 alignment: Alignment.bottomCenter,
75 child: Container( 116 child: Container(
76 - alignment: Alignment.bottomCenter, 117 + alignment: Alignment.center,
  118 + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
77 height: 100, 119 height: 100,
78 color: Colors.black.withOpacity(0.4), 120 color: Colors.black.withOpacity(0.4),
79 - child: Row(  
80 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
81 - children: [  
82 - Center(  
83 - child: SizedBox(  
84 - width: MediaQuery.of(context).size.width - 120,  
85 - height: 50,  
86 - child: FittedBox(  
87 - child: Text(  
88 - barcode?.displayValue ?? 'Scan something!',  
89 - overflow: TextOverflow.fade,  
90 - style: Theme.of(context)  
91 - .textTheme  
92 - .headlineMedium!  
93 - .copyWith(color: Colors.white),  
94 - ),  
95 - ), 121 + child: ScannedBarcodeLabel(barcodes: controller.barcodes),
96 ), 122 ),
97 ), 123 ),
98 ], 124 ],
99 ), 125 ),
100 - ),  
101 - ),  
102 - ],  
103 - );  
104 - },  
105 - ),  
106 ); 126 );
107 } 127 }
  128 +
  129 + @override
  130 + Future<void> dispose() async {
  131 + super.dispose();
  132 + await controller.dispose();
  133 + }
108 } 134 }
109 135
110 class ScannerOverlay extends CustomPainter { 136 class ScannerOverlay extends CustomPainter {
@@ -114,6 +140,8 @@ class ScannerOverlay extends CustomPainter { @@ -114,6 +140,8 @@ class ScannerOverlay extends CustomPainter {
114 140
115 @override 141 @override
116 void paint(Canvas canvas, Size size) { 142 void paint(Canvas canvas, Size size) {
  143 + // TODO: use `Offset.zero & size` instead of Rect.largest
  144 + // we need to pass the size to the custom paint widget
117 final backgroundPath = Path()..addRect(Rect.largest); 145 final backgroundPath = Path()..addRect(Rect.largest);
118 final cutoutPath = Path()..addRect(scanWindow); 146 final cutoutPath = Path()..addRect(scanWindow);
119 147
@@ -138,24 +166,26 @@ class ScannerOverlay extends CustomPainter { @@ -138,24 +166,26 @@ class ScannerOverlay extends CustomPainter {
138 166
139 class BarcodeOverlay extends CustomPainter { 167 class BarcodeOverlay extends CustomPainter {
140 BarcodeOverlay({ 168 BarcodeOverlay({
141 - required this.barcode,  
142 - required this.arguments, 169 + required this.barcodeCorners,
  170 + required this.barcodeSize,
143 required this.boxFit, 171 required this.boxFit,
144 - required this.capture, 172 + required this.cameraPreviewSize,
145 }); 173 });
146 174
147 - final BarcodeCapture capture;  
148 - final Barcode barcode;  
149 - final MobileScannerArguments arguments; 175 + final List<Offset> barcodeCorners;
  176 + final Size barcodeSize;
150 final BoxFit boxFit; 177 final BoxFit boxFit;
  178 + final Size cameraPreviewSize;
151 179
152 @override 180 @override
153 void paint(Canvas canvas, Size size) { 181 void paint(Canvas canvas, Size size) {
154 - if (barcode.corners.isEmpty) { 182 + if (barcodeCorners.isEmpty ||
  183 + barcodeSize.isEmpty ||
  184 + cameraPreviewSize.isEmpty) {
155 return; 185 return;
156 } 186 }
157 187
158 - final adjustedSize = applyBoxFit(boxFit, arguments.size, size); 188 + final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size);
159 189
160 double verticalPadding = size.height - adjustedSize.destination.height; 190 double verticalPadding = size.height - adjustedSize.destination.height;
161 double horizontalPadding = size.width - adjustedSize.destination.width; 191 double horizontalPadding = size.width - adjustedSize.destination.width;
@@ -175,22 +205,21 @@ class BarcodeOverlay extends CustomPainter { @@ -175,22 +205,21 @@ class BarcodeOverlay extends CustomPainter {
175 final double ratioHeight; 205 final double ratioHeight;
176 206
177 if (!kIsWeb && Platform.isIOS) { 207 if (!kIsWeb && Platform.isIOS) {
178 - ratioWidth = capture.size.width / adjustedSize.destination.width;  
179 - ratioHeight = capture.size.height / adjustedSize.destination.height; 208 + ratioWidth = barcodeSize.width / adjustedSize.destination.width;
  209 + ratioHeight = barcodeSize.height / adjustedSize.destination.height;
180 } else { 210 } else {
181 - ratioWidth = arguments.size.width / adjustedSize.destination.width;  
182 - ratioHeight = arguments.size.height / adjustedSize.destination.height; 211 + ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width;
  212 + ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height;
183 } 213 }
184 214
185 - final List<Offset> adjustedOffset = [];  
186 - for (final offset in barcode.corners) {  
187 - adjustedOffset.add( 215 + final List<Offset> adjustedOffset = [
  216 + for (final offset in barcodeCorners)
188 Offset( 217 Offset(
189 offset.dx / ratioWidth + horizontalPadding, 218 offset.dx / ratioWidth + horizontalPadding,
190 offset.dy / ratioHeight + verticalPadding, 219 offset.dy / ratioHeight + verticalPadding,
191 ), 220 ),
192 - );  
193 - } 221 + ];
  222 +
194 final cutoutPath = Path()..addPolygon(adjustedOffset, true); 223 final cutoutPath = Path()..addPolygon(adjustedOffset, true);
195 224
196 final backgroundPaint = Paint() 225 final backgroundPaint = Paint()
1 -import 'package:flutter/material.dart';  
2 -import 'package:mobile_scanner/mobile_scanner.dart';  
3 -import 'package:mobile_scanner_example/scanner_error_widget.dart';  
4 -  
5 -class BarcodeScannerWithoutController extends StatefulWidget {  
6 - const BarcodeScannerWithoutController({super.key});  
7 -  
8 - @override  
9 - State<BarcodeScannerWithoutController> createState() =>  
10 - _BarcodeScannerWithoutControllerState();  
11 -}  
12 -  
13 -class _BarcodeScannerWithoutControllerState  
14 - extends State<BarcodeScannerWithoutController>  
15 - with SingleTickerProviderStateMixin {  
16 - BarcodeCapture? capture;  
17 -  
18 - @override  
19 - Widget build(BuildContext context) {  
20 - return Scaffold(  
21 - appBar: AppBar(title: const Text('Without controller')),  
22 - backgroundColor: Colors.black,  
23 - body: Builder(  
24 - builder: (context) {  
25 - return Stack(  
26 - children: [  
27 - MobileScanner(  
28 - fit: BoxFit.contain,  
29 - errorBuilder: (context, error, child) {  
30 - return ScannerErrorWidget(error: error);  
31 - },  
32 - onDetect: (capture) {  
33 - setState(() {  
34 - this.capture = capture;  
35 - });  
36 - },  
37 - ),  
38 - Align(  
39 - alignment: Alignment.bottomCenter,  
40 - child: Container(  
41 - alignment: Alignment.bottomCenter,  
42 - height: 100,  
43 - color: Colors.black.withOpacity(0.4),  
44 - child: Row(  
45 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
46 - children: [  
47 - Center(  
48 - child: SizedBox(  
49 - width: MediaQuery.of(context).size.width - 120,  
50 - height: 50,  
51 - child: FittedBox(  
52 - child: Text(  
53 - capture?.barcodes.first.rawValue ??  
54 - 'Scan something!',  
55 - overflow: TextOverflow.fade,  
56 - style: Theme.of(context)  
57 - .textTheme  
58 - .headlineMedium!  
59 - .copyWith(color: Colors.white),  
60 - ),  
61 - ),  
62 - ),  
63 - ),  
64 - ],  
65 - ),  
66 - ),  
67 - ),  
68 - ],  
69 - );  
70 - },  
71 - ),  
72 - );  
73 - }  
74 -}  
  1 +import 'dart:async';
  2 +
  3 +import 'package:flutter/foundation.dart';
1 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
2 -import 'package:image_picker/image_picker.dart';  
3 import 'package:mobile_scanner/mobile_scanner.dart'; 5 import 'package:mobile_scanner/mobile_scanner.dart';
  6 +import 'package:mobile_scanner_example/scanned_barcode_label.dart';
4 7
  8 +import 'package:mobile_scanner_example/scanner_button_widgets.dart';
5 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 9 import 'package:mobile_scanner_example/scanner_error_widget.dart';
6 10
7 class BarcodeScannerWithZoom extends StatefulWidget { 11 class BarcodeScannerWithZoom extends StatefulWidget {
@@ -11,64 +15,44 @@ class BarcodeScannerWithZoom extends StatefulWidget { @@ -11,64 +15,44 @@ class BarcodeScannerWithZoom extends StatefulWidget {
11 State<BarcodeScannerWithZoom> createState() => _BarcodeScannerWithZoomState(); 15 State<BarcodeScannerWithZoom> createState() => _BarcodeScannerWithZoomState();
12 } 16 }
13 17
14 -class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>  
15 - with SingleTickerProviderStateMixin {  
16 - BarcodeCapture? barcode;  
17 -  
18 - MobileScannerController controller = MobileScannerController( 18 +class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> {
  19 + final MobileScannerController controller = MobileScannerController(
19 torchEnabled: true, 20 torchEnabled: true,
20 ); 21 );
21 22
22 - bool isStarted = true;  
23 double _zoomFactor = 0.0; 23 double _zoomFactor = 0.0;
24 24
25 @override 25 @override
26 - Widget build(BuildContext context) {  
27 - return Scaffold(  
28 - appBar: AppBar(title: const Text('With zoom slider')),  
29 - backgroundColor: Colors.black,  
30 - body: Builder(  
31 - builder: (context) {  
32 - return Stack(  
33 - children: [  
34 - MobileScanner(  
35 - controller: controller,  
36 - fit: BoxFit.contain,  
37 - errorBuilder: (context, error, child) {  
38 - return ScannerErrorWidget(error: error);  
39 - },  
40 - onDetect: (barcode) {  
41 - setState(() {  
42 - this.barcode = barcode;  
43 - });  
44 - },  
45 - ),  
46 - Align(  
47 - alignment: Alignment.bottomCenter,  
48 - child: Container(  
49 - alignment: Alignment.bottomCenter,  
50 - height: 100,  
51 - color: Colors.black.withOpacity(0.4),  
52 - child: Column(  
53 - children: [  
54 - Padding( 26 + void initState() {
  27 + super.initState();
  28 + controller.start();
  29 + }
  30 +
  31 + Widget _buildZoomScaleSlider() {
  32 + return ValueListenableBuilder(
  33 + valueListenable: controller,
  34 + builder: (context, state, child) {
  35 + if (!state.isInitialized || !state.isRunning) {
  36 + return const SizedBox.shrink();
  37 + }
  38 +
  39 + final TextStyle labelStyle = Theme.of(context)
  40 + .textTheme
  41 + .headlineMedium!
  42 + .copyWith(color: Colors.white);
  43 +
  44 + return Padding(
55 padding: const EdgeInsets.symmetric(horizontal: 8.0), 45 padding: const EdgeInsets.symmetric(horizontal: 8.0),
56 child: Row( 46 child: Row(
57 children: [ 47 children: [
58 Text( 48 Text(
59 - "0%", 49 + '0%',
60 overflow: TextOverflow.fade, 50 overflow: TextOverflow.fade,
61 - style: Theme.of(context)  
62 - .textTheme  
63 - .headlineMedium!  
64 - .copyWith(color: Colors.white), 51 + style: labelStyle,
65 ), 52 ),
66 Expanded( 53 Expanded(
67 child: Slider( 54 child: Slider(
68 - max: 100,  
69 - divisions: 100,  
70 value: _zoomFactor, 55 value: _zoomFactor,
71 - label: "${_zoomFactor.round()} %",  
72 onChanged: (value) { 56 onChanged: (value) {
73 setState(() { 57 setState(() {
74 _zoomFactor = value; 58 _zoomFactor = value;
@@ -78,118 +62,54 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> @@ -78,118 +62,54 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
78 ), 62 ),
79 ), 63 ),
80 Text( 64 Text(
81 - "100%", 65 + '100%',
82 overflow: TextOverflow.fade, 66 overflow: TextOverflow.fade,
83 - style: Theme.of(context)  
84 - .textTheme  
85 - .headlineMedium!  
86 - .copyWith(color: Colors.white), 67 + style: labelStyle,
87 ), 68 ),
88 ], 69 ],
89 ), 70 ),
90 - ),  
91 - Row(  
92 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
93 - children: [  
94 - IconButton(  
95 - color: Colors.white,  
96 - icon: ValueListenableBuilder<TorchState>(  
97 - valueListenable: controller.torchState,  
98 - builder: (context, state, child) {  
99 - switch (state) {  
100 - case TorchState.off:  
101 - return const Icon(  
102 - Icons.flash_off,  
103 - color: Colors.grey,  
104 - );  
105 - case TorchState.on:  
106 - return const Icon(  
107 - Icons.flash_on,  
108 - color: Colors.yellow,  
109 ); 71 );
110 - }  
111 }, 72 },
112 - ),  
113 - iconSize: 32.0,  
114 - onPressed: () => controller.toggleTorch(),  
115 - ),  
116 - IconButton(  
117 - color: Colors.white,  
118 - icon: isStarted  
119 - ? const Icon(Icons.stop)  
120 - : const Icon(Icons.play_arrow),  
121 - iconSize: 32.0,  
122 - onPressed: () => setState(() {  
123 - isStarted  
124 - ? controller.stop()  
125 - : controller.start();  
126 - isStarted = !isStarted;  
127 - }),  
128 - ),  
129 - Center(  
130 - child: SizedBox(  
131 - width: MediaQuery.of(context).size.width - 200,  
132 - height: 50,  
133 - child: FittedBox(  
134 - child: Text(  
135 - barcode?.barcodes.first.rawValue ??  
136 - 'Scan something!',  
137 - overflow: TextOverflow.fade,  
138 - style: Theme.of(context)  
139 - .textTheme  
140 - .headlineMedium!  
141 - .copyWith(color: Colors.white),  
142 - ),  
143 - ),  
144 - ),  
145 - ),  
146 - IconButton(  
147 - color: Colors.white,  
148 - icon: ValueListenableBuilder<CameraFacing>(  
149 - valueListenable: controller.cameraFacingState,  
150 - builder: (context, state, child) {  
151 - switch (state) {  
152 - case CameraFacing.front:  
153 - return const Icon(Icons.camera_front);  
154 - case CameraFacing.back:  
155 - return const Icon(Icons.camera_rear); 73 + );
156 } 74 }
  75 +
  76 + @override
  77 + Widget build(BuildContext context) {
  78 + return Scaffold(
  79 + appBar: AppBar(title: const Text('With zoom slider')),
  80 + backgroundColor: Colors.black,
  81 + body: Stack(
  82 + children: [
  83 + MobileScanner(
  84 + controller: controller,
  85 + fit: BoxFit.contain,
  86 + errorBuilder: (context, error, child) {
  87 + return ScannerErrorWidget(error: error);
157 }, 88 },
158 ), 89 ),
159 - iconSize: 32.0,  
160 - onPressed: () => controller.switchCamera(),  
161 - ),  
162 - IconButton(  
163 - color: Colors.white,  
164 - icon: const Icon(Icons.image),  
165 - iconSize: 32.0,  
166 - onPressed: () async {  
167 - final ImagePicker picker = ImagePicker();  
168 - // Pick an image  
169 - final XFile? image = await picker.pickImage(  
170 - source: ImageSource.gallery,  
171 - );  
172 - if (image != null) {  
173 - if (await controller.analyzeImage(image.path)) {  
174 - if (!context.mounted) return;  
175 - ScaffoldMessenger.of(context).showSnackBar(  
176 - const SnackBar(  
177 - content: Text('Barcode found!'),  
178 - backgroundColor: Colors.green, 90 + Align(
  91 + alignment: Alignment.bottomCenter,
  92 + child: Container(
  93 + alignment: Alignment.bottomCenter,
  94 + height: 100,
  95 + color: Colors.black.withOpacity(0.4),
  96 + child: Column(
  97 + children: [
  98 + if (!kIsWeb) _buildZoomScaleSlider(),
  99 + Row(
  100 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  101 + children: [
  102 + ToggleFlashlightButton(controller: controller),
  103 + StartStopMobileScannerButton(controller: controller),
  104 + Expanded(
  105 + child: Center(
  106 + child: ScannedBarcodeLabel(
  107 + barcodes: controller.barcodes,
179 ), 108 ),
180 - );  
181 - } else {  
182 - if (!context.mounted) return;  
183 - ScaffoldMessenger.of(context).showSnackBar(  
184 - const SnackBar(  
185 - content: Text('No barcode found!'),  
186 - backgroundColor: Colors.red,  
187 ), 109 ),
188 - );  
189 - }  
190 - }  
191 - },  
192 ), 110 ),
  111 + SwitchCameraButton(controller: controller),
  112 + AnalyzeImageFromGalleryButton(controller: controller),
193 ], 113 ],
194 ), 114 ),
195 ], 115 ],
@@ -197,9 +117,13 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> @@ -197,9 +117,13 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
197 ), 117 ),
198 ), 118 ),
199 ], 119 ],
200 - );  
201 - },  
202 ), 120 ),
203 ); 121 );
204 } 122 }
  123 +
  124 + @override
  125 + Future<void> dispose() async {
  126 + super.dispose();
  127 + await controller.dispose();
  128 + }
205 } 129 }
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 -import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';  
3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 2 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
  3 +import 'package:mobile_scanner_example/barcode_scanner_listview.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_pageview.dart';
5 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
6 import 'package:mobile_scanner_example/barcode_scanner_window.dart'; 6 import 'package:mobile_scanner_example/barcode_scanner_window.dart';
7 -import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';  
8 import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; 7 import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
9 import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; 8 import 'package:mobile_scanner_example/mobile_scanner_overlay.dart';
10 9
11 -void main() => runApp(const MaterialApp(home: MyHome())); 10 +void main() {
  11 + runApp(
  12 + const MaterialApp(
  13 + title: 'Mobile Scanner Example',
  14 + home: MyHome(),
  15 + ),
  16 + );
  17 +}
12 18
13 class MyHome extends StatelessWidget { 19 class MyHome extends StatelessWidget {
14 const MyHome({super.key}); 20 const MyHome({super.key});
@@ -16,23 +22,20 @@ class MyHome extends StatelessWidget { @@ -16,23 +22,20 @@ class MyHome extends StatelessWidget {
16 @override 22 @override
17 Widget build(BuildContext context) { 23 Widget build(BuildContext context) {
18 return Scaffold( 24 return Scaffold(
19 - appBar: AppBar(title: const Text('Flutter Demo Home Page')),  
20 - body: SizedBox(  
21 - width: MediaQuery.of(context).size.width,  
22 - height: MediaQuery.of(context).size.height, 25 + appBar: AppBar(title: const Text('Mobile Scanner Example')),
  26 + body: Center(
23 child: Column( 27 child: Column(
24 - mainAxisAlignment: MainAxisAlignment.center, 28 + mainAxisAlignment: MainAxisAlignment.spaceAround,
25 children: [ 29 children: [
26 ElevatedButton( 30 ElevatedButton(
27 onPressed: () { 31 onPressed: () {
28 Navigator.of(context).push( 32 Navigator.of(context).push(
29 MaterialPageRoute( 33 MaterialPageRoute(
30 - builder: (context) =>  
31 - const BarcodeListScannerWithController(), 34 + builder: (context) => const BarcodeScannerListView(),
32 ), 35 ),
33 ); 36 );
34 }, 37 },
35 - child: const Text('MobileScanner with List Controller'), 38 + child: const Text('MobileScanner with ListView'),
36 ), 39 ),
37 ElevatedButton( 40 ElevatedButton(
38 onPressed: () { 41 onPressed: () {
@@ -62,19 +65,9 @@ class MyHome extends StatelessWidget { @@ -62,19 +65,9 @@ class MyHome extends StatelessWidget {
62 ), 65 ),
63 ); 66 );
64 }, 67 },
65 - child:  
66 - const Text('MobileScanner with Controller (returning image)'),  
67 - ),  
68 - ElevatedButton(  
69 - onPressed: () {  
70 - Navigator.of(context).push(  
71 - MaterialPageRoute(  
72 - builder: (context) =>  
73 - const BarcodeScannerWithoutController(), 68 + child: const Text(
  69 + 'MobileScanner with Controller (returning image)',
74 ), 70 ),
75 - );  
76 - },  
77 - child: const Text('MobileScanner without Controller'),  
78 ), 71 ),
79 ElevatedButton( 72 ElevatedButton(
80 onPressed: () { 73 onPressed: () {
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:mobile_scanner/mobile_scanner.dart'; 2 import 'package:mobile_scanner/mobile_scanner.dart';
  3 +import 'package:mobile_scanner_example/scanned_barcode_label.dart';
  4 +import 'package:mobile_scanner_example/scanner_button_widgets.dart';
3 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 5 import 'package:mobile_scanner_example/scanner_error_widget.dart';
4 6
5 class BarcodeScannerWithOverlay extends StatefulWidget { 7 class BarcodeScannerWithOverlay extends StatefulWidget {
@@ -9,174 +11,105 @@ class BarcodeScannerWithOverlay extends StatefulWidget { @@ -9,174 +11,105 @@ class BarcodeScannerWithOverlay extends StatefulWidget {
9 } 11 }
10 12
11 class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> { 13 class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> {
12 - String overlayText = "Please scan QR Code";  
13 - bool camStarted = false;  
14 -  
15 final MobileScannerController controller = MobileScannerController( 14 final MobileScannerController controller = MobileScannerController(
16 formats: const [BarcodeFormat.qrCode], 15 formats: const [BarcodeFormat.qrCode],
17 - autoStart: false,  
18 ); 16 );
19 17
20 @override 18 @override
21 - void dispose() {  
22 - controller.dispose();  
23 - super.dispose();  
24 - }  
25 -  
26 - void startCamera() {  
27 - if (camStarted) {  
28 - return;  
29 - }  
30 -  
31 - controller.start().then((_) {  
32 - if (mounted) {  
33 - setState(() {  
34 - camStarted = true;  
35 - });  
36 - }  
37 - }).catchError((Object error, StackTrace stackTrace) {  
38 - if (mounted) {  
39 - ScaffoldMessenger.of(context).showSnackBar(  
40 - SnackBar(  
41 - content: Text('Something went wrong! $error'),  
42 - backgroundColor: Colors.red,  
43 - ),  
44 - );  
45 - }  
46 - });  
47 - }  
48 -  
49 - void onBarcodeDetect(BarcodeCapture barcodeCapture) {  
50 - final barcode = barcodeCapture.barcodes.last;  
51 - setState(() {  
52 - overlayText = barcodeCapture.barcodes.last.displayValue ??  
53 - barcode.rawValue ??  
54 - 'Barcode has no displayable value';  
55 - }); 19 + void initState() {
  20 + super.initState();
  21 + controller.start();
56 } 22 }
57 23
58 @override 24 @override
59 Widget build(BuildContext context) { 25 Widget build(BuildContext context) {
60 final scanWindow = Rect.fromCenter( 26 final scanWindow = Rect.fromCenter(
61 - center: MediaQuery.of(context).size.center(Offset.zero), 27 + center: MediaQuery.sizeOf(context).center(Offset.zero),
62 width: 200, 28 width: 200,
63 height: 200, 29 height: 200,
64 ); 30 );
65 31
66 return Scaffold( 32 return Scaffold(
  33 + backgroundColor: Colors.black,
67 appBar: AppBar( 34 appBar: AppBar(
68 title: const Text('Scanner with Overlay Example app'), 35 title: const Text('Scanner with Overlay Example app'),
69 ), 36 ),
70 - body: Center(  
71 - child: Column(  
72 - mainAxisAlignment: MainAxisAlignment.center,  
73 - children: <Widget>[  
74 - Expanded(  
75 - child: camStarted  
76 - ? Stack( 37 + body: Stack(
77 fit: StackFit.expand, 38 fit: StackFit.expand,
78 children: [ 39 children: [
79 Center( 40 Center(
80 child: MobileScanner( 41 child: MobileScanner(
81 fit: BoxFit.contain, 42 fit: BoxFit.contain,
82 - onDetect: onBarcodeDetect,  
83 - overlay: Padding(  
84 - padding: const EdgeInsets.all(16.0),  
85 - child: Align(  
86 - alignment: Alignment.bottomCenter,  
87 - child: Opacity(  
88 - opacity: 0.7,  
89 - child: Text(  
90 - overlayText,  
91 - style: const TextStyle(  
92 - backgroundColor: Colors.black26,  
93 - color: Colors.white,  
94 - fontWeight: FontWeight.bold,  
95 - fontSize: 24,  
96 - overflow: TextOverflow.ellipsis,  
97 - ),  
98 - maxLines: 1,  
99 - ),  
100 - ),  
101 - ),  
102 - ),  
103 controller: controller, 43 controller: controller,
104 scanWindow: scanWindow, 44 scanWindow: scanWindow,
105 errorBuilder: (context, error, child) { 45 errorBuilder: (context, error, child) {
106 return ScannerErrorWidget(error: error); 46 return ScannerErrorWidget(error: error);
107 }, 47 },
108 - ),  
109 - ),  
110 - CustomPaint(  
111 - painter: ScannerOverlay(scanWindow),  
112 - ),  
113 - Padding( 48 + overlayBuilder: (context, constraints) {
  49 + return Padding(
114 padding: const EdgeInsets.all(16.0), 50 padding: const EdgeInsets.all(16.0),
115 child: Align( 51 child: Align(
116 alignment: Alignment.bottomCenter, 52 alignment: Alignment.bottomCenter,
117 - child: Row(  
118 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
119 - children: [  
120 - ValueListenableBuilder<TorchState>(  
121 - valueListenable: controller.torchState,  
122 - builder: (context, value, child) {  
123 - final Color iconColor;  
124 -  
125 - switch (value) {  
126 - case TorchState.off:  
127 - iconColor = Colors.black;  
128 - case TorchState.on:  
129 - iconColor = Colors.yellow;  
130 - }  
131 -  
132 - return IconButton(  
133 - onPressed: () => controller.toggleTorch(),  
134 - icon: Icon(  
135 - Icons.flashlight_on,  
136 - color: iconColor, 53 + child: ScannedBarcodeLabel(barcodes: controller.barcodes),
137 ), 54 ),
138 ); 55 );
139 }, 56 },
140 ), 57 ),
141 - IconButton(  
142 - onPressed: () => controller.switchCamera(),  
143 - icon: const Icon(  
144 - Icons.cameraswitch_rounded,  
145 - color: Colors.white,  
146 ), 58 ),
  59 + ValueListenableBuilder(
  60 + valueListenable: controller,
  61 + builder: (context, value, child) {
  62 + if (!value.isInitialized ||
  63 + !value.isRunning ||
  64 + value.error != null) {
  65 + return const SizedBox();
  66 + }
  67 +
  68 + return CustomPaint(
  69 + painter: ScannerOverlay(scanWindow: scanWindow),
  70 + );
  71 + },
147 ), 72 ),
  73 + Align(
  74 + alignment: Alignment.bottomCenter,
  75 + child: Padding(
  76 + padding: const EdgeInsets.all(16.0),
  77 + child: Row(
  78 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  79 + children: [
  80 + ToggleFlashlightButton(controller: controller),
  81 + SwitchCameraButton(controller: controller),
148 ], 82 ],
149 ), 83 ),
150 ), 84 ),
151 ), 85 ),
152 ], 86 ],
153 - )  
154 - : const Center(  
155 - child: Text("Tap on Camera to activate QR Scanner"),  
156 - ),  
157 - ),  
158 - ],  
159 - ),  
160 - ),  
161 - floatingActionButton: camStarted  
162 - ? null  
163 - : FloatingActionButton(  
164 - onPressed: startCamera,  
165 - child: const Icon(Icons.camera_alt),  
166 ), 87 ),
167 ); 88 );
168 } 89 }
  90 +
  91 + @override
  92 + Future<void> dispose() async {
  93 + super.dispose();
  94 + await controller.dispose();
  95 + }
169 } 96 }
170 97
171 class ScannerOverlay extends CustomPainter { 98 class ScannerOverlay extends CustomPainter {
172 - ScannerOverlay(this.scanWindow); 99 + const ScannerOverlay({
  100 + required this.scanWindow,
  101 + this.borderRadius = 12.0,
  102 + });
173 103
174 final Rect scanWindow; 104 final Rect scanWindow;
175 - final double borderRadius = 12.0; 105 + final double borderRadius;
176 106
177 @override 107 @override
178 void paint(Canvas canvas, Size size) { 108 void paint(Canvas canvas, Size size) {
  109 + // TODO: use `Offset.zero & size` instead of Rect.largest
  110 + // we need to pass the size to the custom paint widget
179 final backgroundPath = Path()..addRect(Rect.largest); 111 final backgroundPath = Path()..addRect(Rect.largest);
  112 +
180 final cutoutPath = Path() 113 final cutoutPath = Path()
181 ..addRRect( 114 ..addRRect(
182 RRect.fromRectAndCorners( 115 RRect.fromRectAndCorners(
@@ -199,14 +132,11 @@ class ScannerOverlay extends CustomPainter { @@ -199,14 +132,11 @@ class ScannerOverlay extends CustomPainter {
199 cutoutPath, 132 cutoutPath,
200 ); 133 );
201 134
202 - // Create a Paint object for the white border  
203 final borderPaint = Paint() 135 final borderPaint = Paint()
204 ..color = Colors.white 136 ..color = Colors.white
205 ..style = PaintingStyle.stroke 137 ..style = PaintingStyle.stroke
206 - ..strokeWidth = 4.0; // Adjust the border width as needed 138 + ..strokeWidth = 4.0;
207 139
208 - // Calculate the border rectangle with rounded corners  
209 -// Adjust the radius as needed  
210 final borderRect = RRect.fromRectAndCorners( 140 final borderRect = RRect.fromRectAndCorners(
211 scanWindow, 141 scanWindow,
212 topLeft: Radius.circular(borderRadius), 142 topLeft: Radius.circular(borderRadius),
@@ -215,13 +145,16 @@ class ScannerOverlay extends CustomPainter { @@ -215,13 +145,16 @@ class ScannerOverlay extends CustomPainter {
215 bottomRight: Radius.circular(borderRadius), 145 bottomRight: Radius.circular(borderRadius),
216 ); 146 );
217 147
218 - // Draw the white border 148 + // First, draw the background,
  149 + // with a cutout area that is a bit larger than the scan window.
  150 + // Finally, draw the scan window itself.
219 canvas.drawPath(backgroundWithCutout, backgroundPaint); 151 canvas.drawPath(backgroundWithCutout, backgroundPaint);
220 canvas.drawRRect(borderRect, borderPaint); 152 canvas.drawRRect(borderRect, borderPaint);
221 } 153 }
222 154
223 @override 155 @override
224 - bool shouldRepaint(covariant CustomPainter oldDelegate) {  
225 - return false; 156 + bool shouldRepaint(ScannerOverlay oldDelegate) {
  157 + return scanWindow != oldDelegate.scanWindow ||
  158 + borderRadius != oldDelegate.borderRadius;
226 } 159 }
227 } 160 }
  1 +import 'package:flutter/material.dart';
  2 +import 'package:mobile_scanner/mobile_scanner.dart';
  3 +
  4 +class ScannedBarcodeLabel extends StatelessWidget {
  5 + const ScannedBarcodeLabel({
  6 + super.key,
  7 + required this.barcodes,
  8 + });
  9 +
  10 + final Stream<BarcodeCapture> barcodes;
  11 +
  12 + @override
  13 + Widget build(BuildContext context) {
  14 + return StreamBuilder(
  15 + stream: barcodes,
  16 + builder: (context, snaphot) {
  17 + final scannedBarcodes = snaphot.data?.barcodes ?? [];
  18 +
  19 + if (scannedBarcodes.isEmpty) {
  20 + return const Text(
  21 + 'Scan something!',
  22 + overflow: TextOverflow.fade,
  23 + style: TextStyle(color: Colors.white),
  24 + );
  25 + }
  26 +
  27 + return Text(
  28 + scannedBarcodes.first.displayValue ?? 'No display value.',
  29 + overflow: TextOverflow.fade,
  30 + style: const TextStyle(color: Colors.white),
  31 + );
  32 + },
  33 + );
  34 + }
  35 +}
  1 +import 'package:flutter/material.dart';
  2 +import 'package:image_picker/image_picker.dart';
  3 +import 'package:mobile_scanner/mobile_scanner.dart';
  4 +
  5 +class AnalyzeImageFromGalleryButton extends StatelessWidget {
  6 + const AnalyzeImageFromGalleryButton({required this.controller, super.key});
  7 +
  8 + final MobileScannerController controller;
  9 +
  10 + @override
  11 + Widget build(BuildContext context) {
  12 + return IconButton(
  13 + color: Colors.white,
  14 + icon: const Icon(Icons.image),
  15 + iconSize: 32.0,
  16 + onPressed: () async {
  17 + final ImagePicker picker = ImagePicker();
  18 +
  19 + final XFile? image = await picker.pickImage(
  20 + source: ImageSource.gallery,
  21 + );
  22 +
  23 + if (image == null) {
  24 + return;
  25 + }
  26 +
  27 + final BarcodeCapture? barcodes = await controller.analyzeImage(
  28 + image.path,
  29 + );
  30 +
  31 + if (!context.mounted) {
  32 + return;
  33 + }
  34 +
  35 + final SnackBar snackbar = barcodes != null
  36 + ? const SnackBar(
  37 + content: Text('Barcode found!'),
  38 + backgroundColor: Colors.green,
  39 + )
  40 + : const SnackBar(
  41 + content: Text('No barcode found!'),
  42 + backgroundColor: Colors.red,
  43 + );
  44 +
  45 + ScaffoldMessenger.of(context).showSnackBar(snackbar);
  46 + },
  47 + );
  48 + }
  49 +}
  50 +
  51 +class StartStopMobileScannerButton extends StatelessWidget {
  52 + const StartStopMobileScannerButton({required this.controller, super.key});
  53 +
  54 + final MobileScannerController controller;
  55 +
  56 + @override
  57 + Widget build(BuildContext context) {
  58 + return ValueListenableBuilder(
  59 + valueListenable: controller,
  60 + builder: (context, state, child) {
  61 + if (!state.isInitialized || !state.isRunning) {
  62 + return IconButton(
  63 + color: Colors.white,
  64 + icon: const Icon(Icons.play_arrow),
  65 + iconSize: 32.0,
  66 + onPressed: () async {
  67 + await controller.start();
  68 + },
  69 + );
  70 + }
  71 +
  72 + return IconButton(
  73 + color: Colors.white,
  74 + icon: const Icon(Icons.stop),
  75 + iconSize: 32.0,
  76 + onPressed: () async {
  77 + await controller.stop();
  78 + },
  79 + );
  80 + },
  81 + );
  82 + }
  83 +}
  84 +
  85 +class SwitchCameraButton extends StatelessWidget {
  86 + const SwitchCameraButton({required this.controller, super.key});
  87 +
  88 + final MobileScannerController controller;
  89 +
  90 + @override
  91 + Widget build(BuildContext context) {
  92 + return ValueListenableBuilder(
  93 + valueListenable: controller,
  94 + builder: (context, state, child) {
  95 + if (!state.isInitialized || !state.isRunning) {
  96 + return const SizedBox.shrink();
  97 + }
  98 +
  99 + final int? availableCameras = state.availableCameras;
  100 +
  101 + if (availableCameras != null && availableCameras < 2) {
  102 + return const SizedBox.shrink();
  103 + }
  104 +
  105 + final Widget icon;
  106 +
  107 + switch (state.cameraDirection) {
  108 + case CameraFacing.front:
  109 + icon = const Icon(Icons.camera_front);
  110 + case CameraFacing.back:
  111 + icon = const Icon(Icons.camera_rear);
  112 + }
  113 +
  114 + return IconButton(
  115 + iconSize: 32.0,
  116 + icon: icon,
  117 + onPressed: () async {
  118 + await controller.switchCamera();
  119 + },
  120 + );
  121 + },
  122 + );
  123 + }
  124 +}
  125 +
  126 +class ToggleFlashlightButton extends StatelessWidget {
  127 + const ToggleFlashlightButton({required this.controller, super.key});
  128 +
  129 + final MobileScannerController controller;
  130 +
  131 + @override
  132 + Widget build(BuildContext context) {
  133 + return ValueListenableBuilder(
  134 + valueListenable: controller,
  135 + builder: (context, state, child) {
  136 + if (!state.isInitialized || !state.isRunning) {
  137 + return const SizedBox.shrink();
  138 + }
  139 +
  140 + switch (state.torchState) {
  141 + case TorchState.off:
  142 + return IconButton(
  143 + color: Colors.white,
  144 + iconSize: 32.0,
  145 + icon: const Icon(Icons.flash_off),
  146 + onPressed: () async {
  147 + await controller.toggleTorch();
  148 + },
  149 + );
  150 + case TorchState.on:
  151 + return IconButton(
  152 + color: Colors.white,
  153 + iconSize: 32.0,
  154 + icon: const Icon(Icons.flash_on),
  155 + onPressed: () async {
  156 + await controller.toggleTorch();
  157 + },
  158 + );
  159 + case TorchState.unavailable:
  160 + return const Icon(
  161 + Icons.no_flash,
  162 + color: Colors.grey,
  163 + );
  164 + }
  165 + },
  166 + );
  167 + }
  168 +}
@@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
6 version: 0.0.1 6 version: 0.0.1
7 7
8 environment: 8 environment:
9 - sdk: ">=3.1.0 <4.0.0"  
10 - flutter: ">=3.13.0" 9 + sdk: ">=3.3.0 <4.0.0"
  10 + flutter: ">=3.19.0"
11 11
12 # Dependencies specify other packages that your package needs in order to work. 12 # Dependencies specify other packages that your package needs in order to work.
13 # To automatically upgrade your package dependencies to the latest versions 13 # To automatically upgrade your package dependencies to the latest versions
@@ -40,7 +40,6 @@ @@ -40,7 +40,6 @@
40 <script src="flutter.js" defer></script> 40 <script src="flutter.js" defer></script>
41 </head> 41 </head>
42 <body> 42 <body>
43 - <script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script>  
44 <script> 43 <script>
45 window.addEventListener('load', function(ev) { 44 window.addEventListener('load', function(ev) {
46 // Download main.dart.js 45 // Download main.dart.js
@@ -245,12 +245,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -245,12 +245,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
245 return 245 return
246 } 246 }
247 247
248 - mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in 248 + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in
249 if error != nil { 249 if error != nil {
250 - barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])  
251 -  
252 DispatchQueue.main.async { 250 DispatchQueue.main.async {
253 - result(false) 251 + result(FlutterError(code: "MobileScanner",
  252 + message: error?.localizedDescription,
  253 + details: nil))
254 } 254 }
255 255
256 return 256 return
@@ -258,15 +258,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -258,15 +258,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
258 258
259 if (barcodes == nil || barcodes!.isEmpty) { 259 if (barcodes == nil || barcodes!.isEmpty) {
260 DispatchQueue.main.async { 260 DispatchQueue.main.async {
261 - result(false) 261 + result(nil)
262 } 262 }
263 } else { 263 } else {
264 let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data } 264 let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data }
265 - let event: [String: Any?] = ["name": "barcode", "data": barcodesMap]  
266 - barcodeHandler.publishEvent(event)  
267 265
268 DispatchQueue.main.async { 266 DispatchQueue.main.async {
269 - result(true) 267 + result(["name": "barcode", "data": barcodesMap])
270 } 268 }
271 } 269 }
272 }) 270 })
@@ -5,13 +5,15 @@ export 'src/enums/camera_facing.dart'; @@ -5,13 +5,15 @@ export 'src/enums/camera_facing.dart';
5 export 'src/enums/detection_speed.dart'; 5 export 'src/enums/detection_speed.dart';
6 export 'src/enums/email_type.dart'; 6 export 'src/enums/email_type.dart';
7 export 'src/enums/encryption_type.dart'; 7 export 'src/enums/encryption_type.dart';
  8 +export 'src/enums/mobile_scanner_authorization_state.dart';
8 export 'src/enums/mobile_scanner_error_code.dart'; 9 export 'src/enums/mobile_scanner_error_code.dart';
9 -export 'src/enums/mobile_scanner_state.dart';  
10 export 'src/enums/phone_type.dart'; 10 export 'src/enums/phone_type.dart';
11 export 'src/enums/torch_state.dart'; 11 export 'src/enums/torch_state.dart';
12 export 'src/mobile_scanner.dart'; 12 export 'src/mobile_scanner.dart';
13 export 'src/mobile_scanner_controller.dart'; 13 export 'src/mobile_scanner_controller.dart';
14 -export 'src/mobile_scanner_exception.dart'; 14 +export 'src/mobile_scanner_exception.dart'
  15 + hide PermissionRequestPendingException;
  16 +export 'src/mobile_scanner_platform_interface.dart';
15 export 'src/objects/address.dart'; 17 export 'src/objects/address.dart';
16 export 'src/objects/barcode.dart'; 18 export 'src/objects/barcode.dart';
17 export 'src/objects/barcode_capture.dart'; 19 export 'src/objects/barcode_capture.dart';
@@ -20,7 +22,7 @@ export 'src/objects/contact_info.dart'; @@ -20,7 +22,7 @@ export 'src/objects/contact_info.dart';
20 export 'src/objects/driver_license.dart'; 22 export 'src/objects/driver_license.dart';
21 export 'src/objects/email.dart'; 23 export 'src/objects/email.dart';
22 export 'src/objects/geo_point.dart'; 24 export 'src/objects/geo_point.dart';
23 -export 'src/objects/mobile_scanner_arguments.dart'; 25 +export 'src/objects/mobile_scanner_state.dart';
24 export 'src/objects/person_name.dart'; 26 export 'src/objects/person_name.dart';
25 export 'src/objects/phone.dart'; 27 export 'src/objects/phone.dart';
26 export 'src/objects/sms.dart'; 28 export 'src/objects/sms.dart';
1 -export 'src/web/base.dart';  
2 -export 'src/web/jsqr.dart';  
3 -export 'src/web/zxing.dart';  
1 -import 'dart:async';  
2 -import 'dart:html' as html;  
3 -import 'dart:ui_web' as ui;  
4 -  
5 -import 'package:flutter/services.dart';  
6 -import 'package:flutter_web_plugins/flutter_web_plugins.dart';  
7 -import 'package:mobile_scanner/mobile_scanner_web.dart';  
8 -import 'package:mobile_scanner/src/enums/barcode_format.dart';  
9 -import 'package:mobile_scanner/src/enums/camera_facing.dart';  
10 -  
11 -/// This plugin is the web implementation of mobile_scanner.  
12 -/// It only supports QR codes.  
13 -class MobileScannerWebPlugin {  
14 - static void registerWith(Registrar registrar) {  
15 - final PluginEventChannel event = PluginEventChannel(  
16 - 'dev.steenbakker.mobile_scanner/scanner/event',  
17 - const StandardMethodCodec(),  
18 - registrar,  
19 - );  
20 - final MethodChannel channel = MethodChannel(  
21 - 'dev.steenbakker.mobile_scanner/scanner/method',  
22 - const StandardMethodCodec(),  
23 - registrar,  
24 - );  
25 - final MobileScannerWebPlugin instance = MobileScannerWebPlugin();  
26 -  
27 - channel.setMethodCallHandler(instance.handleMethodCall);  
28 - event.setController(instance.controller);  
29 - }  
30 -  
31 - // Controller to send events back to the framework  
32 - StreamController controller = StreamController.broadcast();  
33 -  
34 - // ID of the video feed  
35 - String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}';  
36 -  
37 - static final html.DivElement vidDiv = html.DivElement();  
38 -  
39 - /// Represents barcode reader library.  
40 - /// Change this property if you want to use a custom implementation.  
41 - ///  
42 - /// Example of using the jsQR library:  
43 - /// void main() {  
44 - /// if (kIsWeb) {  
45 - /// MobileScannerWebPlugin.barCodeReader =  
46 - /// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv);  
47 - /// }  
48 - /// runApp(const MaterialApp(home: MyHome()));  
49 - /// }  
50 - static WebBarcodeReaderBase barCodeReader =  
51 - ZXingBarcodeReader(videoContainer: vidDiv);  
52 - StreamSubscription? _barCodeStreamSubscription;  
53 -  
54 - /// Handle incomming messages  
55 - Future<dynamic> handleMethodCall(MethodCall call) async {  
56 - switch (call.method) {  
57 - case 'start':  
58 - return _start(call.arguments as Map);  
59 - case 'torch':  
60 - return _torch(call.arguments);  
61 - case 'stop':  
62 - return cancel();  
63 - case 'updateScanWindow':  
64 - return Future<void>.value();  
65 - default:  
66 - throw PlatformException(  
67 - code: 'Unimplemented',  
68 - details: "The mobile_scanner plugin for web doesn't implement "  
69 - "the method '${call.method}'",  
70 - );  
71 - }  
72 - }  
73 -  
74 - /// Can enable or disable the flash if available  
75 - Future<void> _torch(arguments) async {  
76 - barCodeReader.toggleTorch(enabled: arguments == 1);  
77 - }  
78 -  
79 - /// Starts the video stream and the scanner  
80 - Future<Map> _start(Map arguments) async {  
81 - var cameraFacing = CameraFacing.front;  
82 - if (arguments.containsKey('facing')) {  
83 - cameraFacing = CameraFacing.values[arguments['facing'] as int];  
84 - }  
85 -  
86 - ui.platformViewRegistry.registerViewFactory(  
87 - viewID,  
88 - (int id) {  
89 - return vidDiv  
90 - ..style.width = '100%'  
91 - ..style.height = '100%';  
92 - },  
93 - );  
94 -  
95 - // Check if stream is running  
96 - if (barCodeReader.isStarted) {  
97 - final hasTorch = await barCodeReader.hasTorch();  
98 - return {  
99 - 'ViewID': viewID,  
100 - 'videoWidth': barCodeReader.videoWidth,  
101 - 'videoHeight': barCodeReader.videoHeight,  
102 - 'torchable': hasTorch,  
103 - };  
104 - }  
105 - try {  
106 - List<BarcodeFormat>? formats;  
107 - if (arguments.containsKey('formats')) {  
108 - formats = (arguments['formats'] as List)  
109 - .cast<int>()  
110 - .map(BarcodeFormat.fromRawValue)  
111 - .toList();  
112 - }  
113 -  
114 - final Duration? detectionTimeout;  
115 - if (arguments.containsKey('timeout')) {  
116 - detectionTimeout = Duration(milliseconds: arguments['timeout'] as int);  
117 - } else {  
118 - detectionTimeout = null;  
119 - }  
120 -  
121 - await barCodeReader.start(  
122 - cameraFacing: cameraFacing,  
123 - formats: formats,  
124 - detectionTimeout: detectionTimeout,  
125 - );  
126 -  
127 - _barCodeStreamSubscription =  
128 - barCodeReader.detectBarcodeContinuously().listen((code) {  
129 - if (code != null) {  
130 - controller.add({  
131 - 'name': 'barcodeWeb',  
132 - 'data': {  
133 - 'rawValue': code.rawValue,  
134 - 'rawBytes': code.rawBytes,  
135 - 'format': code.format.rawValue,  
136 - 'displayValue': code.displayValue,  
137 - 'type': code.type.rawValue,  
138 - if (code.corners.isNotEmpty)  
139 - 'corners': code.corners  
140 - .map(  
141 - (Offset c) => <Object?, Object?>{'x': c.dx, 'y': c.dy},  
142 - )  
143 - .toList(),  
144 - },  
145 - });  
146 - }  
147 - });  
148 -  
149 - final hasTorch = await barCodeReader.hasTorch();  
150 - final bool? enableTorch = arguments['torch'] as bool?;  
151 -  
152 - if (hasTorch && enableTorch != null) {  
153 - await barCodeReader.toggleTorch(enabled: enableTorch);  
154 - }  
155 -  
156 - return {  
157 - 'ViewID': viewID,  
158 - 'videoWidth': barCodeReader.videoWidth,  
159 - 'videoHeight': barCodeReader.videoHeight,  
160 - 'torchable': hasTorch,  
161 - };  
162 - } catch (e, stackTrace) {  
163 - throw PlatformException(  
164 - code: 'MobileScannerWeb',  
165 - message: '$e',  
166 - details: stackTrace.toString(),  
167 - );  
168 - }  
169 - }  
170 -  
171 - /// Check if any camera's are available  
172 - static Future<bool> cameraAvailable() async {  
173 - final sources =  
174 - await html.window.navigator.mediaDevices!.enumerateDevices();  
175 - for (final e in sources) {  
176 - // TODO:  
177 - // ignore: avoid_dynamic_calls  
178 - if (e.kind == 'videoinput') {  
179 - return true;  
180 - }  
181 - }  
182 - return false;  
183 - }  
184 -  
185 - /// Stops the video feed and analyzer  
186 - Future<void> cancel() async {  
187 - await barCodeReader.stop();  
188 - await barCodeReader.stopDetectBarcodeContinuously();  
189 - await _barCodeStreamSubscription?.cancel();  
190 - _barCodeStreamSubscription = null;  
191 - }  
192 -}  
1 /// The authorization state of the scanner. 1 /// The authorization state of the scanner.
2 -enum MobileScannerState { 2 +enum MobileScannerAuthorizationState {
3 /// The scanner has not yet requested the required permissions. 3 /// The scanner has not yet requested the required permissions.
4 undetermined(0), 4 undetermined(0),
5 5
@@ -9,16 +9,16 @@ enum MobileScannerState { @@ -9,16 +9,16 @@ enum MobileScannerState {
9 /// The user denied the required permissions. 9 /// The user denied the required permissions.
10 denied(2); 10 denied(2);
11 11
12 - const MobileScannerState(this.rawValue); 12 + const MobileScannerAuthorizationState(this.rawValue);
13 13
14 - factory MobileScannerState.fromRawValue(int value) { 14 + factory MobileScannerAuthorizationState.fromRawValue(int value) {
15 switch (value) { 15 switch (value) {
16 case 0: 16 case 0:
17 - return MobileScannerState.undetermined; 17 + return MobileScannerAuthorizationState.undetermined;
18 case 1: 18 case 1:
19 - return MobileScannerState.authorized; 19 + return MobileScannerAuthorizationState.authorized;
20 case 2: 20 case 2:
21 - return MobileScannerState.denied; 21 + return MobileScannerAuthorizationState.denied;
22 default: 22 default:
23 throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 23 throw ArgumentError.value(value, 'value', 'Invalid raw value.');
24 } 24 }
  1 +import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
  2 +
1 /// This enum defines the different error codes for the mobile scanner. 3 /// This enum defines the different error codes for the mobile scanner.
2 enum MobileScannerErrorCode { 4 enum MobileScannerErrorCode {
  5 + /// The controller was already started.
  6 + ///
  7 + /// The controller should be stopped using [MobileScannerController.stop],
  8 + /// before restarting it.
  9 + controllerAlreadyInitialized,
  10 +
  11 + /// The controller was used after being disposed.
  12 + controllerDisposed,
  13 +
3 /// The controller was used 14 /// The controller was used
4 /// while it was not yet initialized using [MobileScannerController.start]. 15 /// while it was not yet initialized using [MobileScannerController.start].
5 controllerUninitialized, 16 controllerUninitialized,
@@ -4,7 +4,10 @@ enum TorchState { @@ -4,7 +4,10 @@ enum TorchState {
4 off(0), 4 off(0),
5 5
6 /// The flashlight is on. 6 /// The flashlight is on.
7 - on(1); 7 + on(1),
  8 +
  9 + /// The flashlight is unavailable.
  10 + unavailable(2);
8 11
9 const TorchState(this.rawValue); 12 const TorchState(this.rawValue);
10 13
@@ -14,6 +17,8 @@ enum TorchState { @@ -14,6 +17,8 @@ enum TorchState {
14 return TorchState.off; 17 return TorchState.off;
15 case 1: 18 case 1:
16 return TorchState.on; 19 return TorchState.on;
  20 + case 2:
  21 + return TorchState.unavailable;
17 default: 22 default:
18 throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 23 throw ArgumentError.value(value, 'value', 'Invalid raw value.');
19 } 24 }
  1 +import 'dart:async';
  2 +import 'dart:io';
  3 +
  4 +import 'package:flutter/services.dart';
  5 +import 'package:flutter/widgets.dart';
  6 +import 'package:mobile_scanner/src/enums/barcode_format.dart';
  7 +import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart';
  8 +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
  9 +import 'package:mobile_scanner/src/enums/torch_state.dart';
  10 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
  11 +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
  12 +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
  13 +import 'package:mobile_scanner/src/objects/barcode.dart';
  14 +import 'package:mobile_scanner/src/objects/barcode_capture.dart';
  15 +import 'package:mobile_scanner/src/objects/start_options.dart';
  16 +
  17 +/// An implementation of [MobileScannerPlatform] that uses method channels.
  18 +class MethodChannelMobileScanner extends MobileScannerPlatform {
  19 + /// The method channel used to interact with the native platform.
  20 + @visibleForTesting
  21 + final methodChannel = const MethodChannel(
  22 + 'dev.steenbakker.mobile_scanner/scanner/method',
  23 + );
  24 +
  25 + /// The event channel that sends back scanned barcode events.
  26 + @visibleForTesting
  27 + final eventChannel = const EventChannel(
  28 + 'dev.steenbakker.mobile_scanner/scanner/event',
  29 + );
  30 +
  31 + Stream<Map<Object?, Object?>>? _eventsStream;
  32 +
  33 + Stream<Map<Object?, Object?>> get eventsStream {
  34 + _eventsStream ??=
  35 + eventChannel.receiveBroadcastStream().cast<Map<Object?, Object?>>();
  36 +
  37 + return _eventsStream!;
  38 + }
  39 +
  40 + int? _textureId;
  41 +
  42 + /// Parse a [BarcodeCapture] from the given [event].
  43 + BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) {
  44 + if (event == null) {
  45 + return null;
  46 + }
  47 +
  48 + final Object? data = event['data'];
  49 +
  50 + if (data == null || data is! List<Object?>) {
  51 + return null;
  52 + }
  53 +
  54 + final List<Map<Object?, Object?>> barcodes =
  55 + data.cast<Map<Object?, Object?>>();
  56 +
  57 + if (Platform.isMacOS) {
  58 + return BarcodeCapture(
  59 + raw: event,
  60 + barcodes: barcodes
  61 + .map(
  62 + (barcode) => Barcode(
  63 + rawValue: barcode['payload'] as String?,
  64 + format: BarcodeFormat.fromRawValue(
  65 + barcode['symbology'] as int? ?? -1,
  66 + ),
  67 + ),
  68 + )
  69 + .toList(),
  70 + );
  71 + }
  72 +
  73 + if (Platform.isAndroid || Platform.isIOS) {
  74 + final double? width = event['width'] as double?;
  75 + final double? height = event['height'] as double?;
  76 +
  77 + return BarcodeCapture(
  78 + raw: data,
  79 + barcodes: barcodes.map(Barcode.fromNative).toList(),
  80 + image: event['image'] as Uint8List?,
  81 + size: width == null || height == null ? Size.zero : Size(width, height),
  82 + );
  83 + }
  84 +
  85 + throw const MobileScannerException(
  86 + errorCode: MobileScannerErrorCode.genericError,
  87 + errorDetails: MobileScannerErrorDetails(
  88 + message: 'Only Android, iOS and macOS are supported.',
  89 + ),
  90 + );
  91 + }
  92 +
  93 + /// Request permission to access the camera.
  94 + ///
  95 + /// Throws a [MobileScannerException] if the permission is not granted.
  96 + Future<void> _requestCameraPermission() async {
  97 + final MobileScannerAuthorizationState authorizationState;
  98 +
  99 + try {
  100 + authorizationState = MobileScannerAuthorizationState.fromRawValue(
  101 + await methodChannel.invokeMethod<int>('state') ?? 0,
  102 + );
  103 + } on PlatformException catch (error) {
  104 + // If the permission state is invalid, that is an error.
  105 + throw MobileScannerException(
  106 + errorCode: MobileScannerErrorCode.genericError,
  107 + errorDetails: MobileScannerErrorDetails(
  108 + code: error.code,
  109 + details: error.details as Object?,
  110 + message: error.message,
  111 + ),
  112 + );
  113 + }
  114 +
  115 + switch (authorizationState) {
  116 + case MobileScannerAuthorizationState.authorized:
  117 + return; // Already authorized.
  118 + // Android does not have an undetermined authorization state.
  119 + // So if the permission was denied, request it again.
  120 + case MobileScannerAuthorizationState.denied:
  121 + case MobileScannerAuthorizationState.undetermined:
  122 + try {
  123 + final bool granted =
  124 + await methodChannel.invokeMethod<bool>('request') ?? false;
  125 +
  126 + if (granted) {
  127 + return; // Authorization was granted.
  128 + }
  129 +
  130 + throw const MobileScannerException(
  131 + errorCode: MobileScannerErrorCode.permissionDenied,
  132 + );
  133 + } on PlatformException catch (error) {
  134 + // If the permission state is invalid, that is an error.
  135 + throw MobileScannerException(
  136 + errorCode: MobileScannerErrorCode.genericError,
  137 + errorDetails: MobileScannerErrorDetails(
  138 + code: error.code,
  139 + details: error.details as Object?,
  140 + message: error.message,
  141 + ),
  142 + );
  143 + }
  144 + }
  145 + }
  146 +
  147 + @override
  148 + Stream<BarcodeCapture?> get barcodesStream {
  149 + return eventsStream
  150 + .where((event) => event['name'] == 'barcode')
  151 + .map((event) => _parseBarcode(event));
  152 + }
  153 +
  154 + @override
  155 + Stream<TorchState> get torchStateStream {
  156 + return eventsStream
  157 + .where((event) => event['name'] == 'torchState')
  158 + .map((event) => TorchState.fromRawValue(event['data'] as int? ?? 0));
  159 + }
  160 +
  161 + @override
  162 + Stream<double> get zoomScaleStateStream {
  163 + return eventsStream
  164 + .where((event) => event['name'] == 'zoomScaleState')
  165 + .map((event) => event['data'] as double? ?? 0.0);
  166 + }
  167 +
  168 + @override
  169 + Future<BarcodeCapture?> analyzeImage(String path) async {
  170 + final Map<String, Object?>? result =
  171 + await methodChannel.invokeMapMethod<String, Object?>(
  172 + 'analyzeImage',
  173 + path,
  174 + );
  175 +
  176 + return _parseBarcode(result);
  177 + }
  178 +
  179 + @override
  180 + Widget buildCameraView() {
  181 + if (_textureId == null) {
  182 + return const SizedBox();
  183 + }
  184 +
  185 + return Texture(textureId: _textureId!);
  186 + }
  187 +
  188 + @override
  189 + Future<void> resetZoomScale() async {
  190 + await methodChannel.invokeMethod<void>('resetScale');
  191 + }
  192 +
  193 + @override
  194 + Future<void> setTorchState(TorchState torchState) async {
  195 + if (torchState == TorchState.unavailable) {
  196 + return;
  197 + }
  198 +
  199 + await methodChannel.invokeMethod<void>('torch', torchState.rawValue);
  200 + }
  201 +
  202 + @override
  203 + Future<void> setZoomScale(double zoomScale) async {
  204 + await methodChannel.invokeMethod<void>('setScale', zoomScale);
  205 + }
  206 +
  207 + @override
  208 + Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
  209 + if (_textureId != null) {
  210 + throw const MobileScannerException(
  211 + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
  212 + errorDetails: MobileScannerErrorDetails(
  213 + message:
  214 + 'The scanner was already started. Call stop() before calling start() again.',
  215 + ),
  216 + );
  217 + }
  218 +
  219 + await _requestCameraPermission();
  220 +
  221 + Map<String, Object?>? startResult;
  222 +
  223 + try {
  224 + startResult = await methodChannel.invokeMapMethod<String, Object?>(
  225 + 'start',
  226 + startOptions.toMap(),
  227 + );
  228 + } on PlatformException catch (error) {
  229 + throw MobileScannerException(
  230 + errorCode: MobileScannerErrorCode.genericError,
  231 + errorDetails: MobileScannerErrorDetails(
  232 + code: error.code,
  233 + details: error.details as Object?,
  234 + message: error.message,
  235 + ),
  236 + );
  237 + }
  238 +
  239 + if (startResult == null) {
  240 + throw const MobileScannerException(
  241 + errorCode: MobileScannerErrorCode.genericError,
  242 + errorDetails: MobileScannerErrorDetails(
  243 + message: 'The start method did not return a view configuration.',
  244 + ),
  245 + );
  246 + }
  247 +
  248 + final int? textureId = startResult['textureId'] as int?;
  249 +
  250 + if (textureId == null) {
  251 + throw const MobileScannerException(
  252 + errorCode: MobileScannerErrorCode.genericError,
  253 + errorDetails: MobileScannerErrorDetails(
  254 + message: 'The start method did not return a texture id.',
  255 + ),
  256 + );
  257 + }
  258 +
  259 + _textureId = textureId;
  260 +
  261 + final int? numberOfCameras = startResult['numberOfCameras'] as int?;
  262 + final bool hasTorch = startResult['torchable'] as bool? ?? false;
  263 +
  264 + final Map<Object?, Object?>? sizeInfo =
  265 + startResult['size'] as Map<Object?, Object?>?;
  266 + final double? width = sizeInfo?['width'] as double?;
  267 + final double? height = sizeInfo?['height'] as double?;
  268 +
  269 + final Size size;
  270 +
  271 + if (width == null || height == null) {
  272 + size = Size.zero;
  273 + } else {
  274 + size = Size(width, height);
  275 + }
  276 +
  277 + return MobileScannerViewAttributes(
  278 + hasTorch: hasTorch,
  279 + numberOfCameras: numberOfCameras,
  280 + size: size,
  281 + );
  282 + }
  283 +
  284 + @override
  285 + Future<void> stop() async {
  286 + if (_textureId == null) {
  287 + return;
  288 + }
  289 +
  290 + _textureId = null;
  291 +
  292 + await methodChannel.invokeMethod<void>('stop');
  293 + }
  294 +
  295 + @override
  296 + Future<void> updateScanWindow(Rect? window) async {
  297 + if (_textureId == null) {
  298 + return;
  299 + }
  300 +
  301 + List<double>? points;
  302 +
  303 + if (window != null) {
  304 + points = [window.left, window.top, window.right, window.bottom];
  305 + }
  306 +
  307 + await methodChannel.invokeMethod<void>(
  308 + 'updateScanWindow',
  309 + {'rect': points},
  310 + );
  311 + }
  312 +
  313 + @override
  314 + Future<void> dispose() async {
  315 + await stop();
  316 + }
  317 +}
1 import 'dart:async'; 1 import 'dart:async';
2 2
3 -import 'package:flutter/foundation.dart';  
4 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
5 -import 'package:flutter/services.dart';  
6 -import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';  
7 import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; 4 import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
8 import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; 5 import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
9 -import 'package:mobile_scanner/src/objects/barcode_capture.dart';  
10 -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; 6 +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
  7 +import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
11 import 'package:mobile_scanner/src/scan_window_calculation.dart'; 8 import 'package:mobile_scanner/src/scan_window_calculation.dart';
12 9
13 /// The function signature for the error builder. 10 /// The function signature for the error builder.
@@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function( @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function(
17 Widget?, 14 Widget?,
18 ); 15 );
19 16
20 -/// The [MobileScanner] widget displays a live camera preview. 17 +/// This widget displays a live camera preview for the barcode scanner.
21 class MobileScanner extends StatefulWidget { 18 class MobileScanner extends StatefulWidget {
22 - /// The controller that manages the barcode scanner.  
23 - ///  
24 - /// If this is null, the scanner will manage its own controller.  
25 - final MobileScannerController? controller; 19 + /// Create a new [MobileScanner] using the provided [controller].
  20 + const MobileScanner({
  21 + required this.controller,
  22 + this.fit = BoxFit.cover,
  23 + this.errorBuilder,
  24 + this.overlayBuilder,
  25 + this.placeholderBuilder,
  26 + this.scanWindow,
  27 + super.key,
  28 + });
  29 +
  30 + /// The controller for the camera preview.
  31 + final MobileScannerController controller;
26 32
27 - /// The function that builds an error widget when the scanner  
28 - /// could not be started. 33 + /// The error builder for the camera preview.
29 /// 34 ///
30 - /// If this is null, defaults to a black [ColoredBox]  
31 - /// with a centered white [Icons.error] icon. 35 + /// If this is null, a black [ColoredBox],
  36 + /// with a centered white [Icons.error] icon is used as error widget.
32 final MobileScannerErrorBuilder? errorBuilder; 37 final MobileScannerErrorBuilder? errorBuilder;
33 38
34 /// The [BoxFit] for the camera preview. 39 /// The [BoxFit] for the camera preview.
@@ -36,250 +41,153 @@ class MobileScanner extends StatefulWidget { @@ -36,250 +41,153 @@ class MobileScanner extends StatefulWidget {
36 /// Defaults to [BoxFit.cover]. 41 /// Defaults to [BoxFit.cover].
37 final BoxFit fit; 42 final BoxFit fit;
38 43
39 - /// The function that signals when new codes were detected by the [controller].  
40 - final void Function(BarcodeCapture barcodes) onDetect;  
41 -  
42 - /// The function that signals when the barcode scanner is started.  
43 - @Deprecated('Use onScannerStarted() instead.')  
44 - final void Function(MobileScannerArguments? arguments)? onStart;  
45 -  
46 - /// The function that signals when the barcode scanner is started.  
47 - final void Function(MobileScannerArguments? arguments)? onScannerStarted; 44 + /// The builder for the overlay above the camera preview.
  45 + ///
  46 + /// The resulting widget can be combined with the [scanWindow] rectangle
  47 + /// to create a cutout for the camera preview.
  48 + ///
  49 + /// The [BoxConstraints] for this builder
  50 + /// are the same constraints that are used to compute the effective [scanWindow].
  51 + ///
  52 + /// The overlay is only displayed when the camera preview is visible.
  53 + final LayoutWidgetBuilder? overlayBuilder;
48 54
49 - /// The function that builds a placeholder widget when the scanner  
50 - /// is not yet displaying its camera preview. 55 + /// The placeholder builder for the camera preview.
51 /// 56 ///
52 /// If this is null, a black [ColoredBox] is used as placeholder. 57 /// If this is null, a black [ColoredBox] is used as placeholder.
  58 + ///
  59 + /// The placeholder is displayed when the camera preview is being initialized.
53 final Widget Function(BuildContext, Widget?)? placeholderBuilder; 60 final Widget Function(BuildContext, Widget?)? placeholderBuilder;
54 61
55 - /// if set barcodes will only be scanned if they fall within this [Rect]  
56 - /// useful for having a cut-out overlay for example. these [Rect]  
57 - /// coordinates are relative to the widget size, so by how much your  
58 - /// rectangle overlays the actual image can depend on things like the  
59 - /// [BoxFit]  
60 - final Rect? scanWindow;  
61 -  
62 - /// Only set this to true if you are starting another instance of mobile_scanner  
63 - /// right after disposing the first one, like in a PageView. 62 + /// The scan window rectangle for the barcode scanner.
64 /// 63 ///
65 - /// Default: false  
66 - final bool startDelay;  
67 -  
68 - /// The overlay which will be painted above the scanner when has started successful.  
69 - /// Will no be pointed when an error occurs or the scanner hasn't been started yet.  
70 - final Widget? overlay;  
71 -  
72 - /// Create a new [MobileScanner] using the provided [controller]  
73 - /// and [onBarcodeDetected] callback.  
74 - const MobileScanner({  
75 - this.controller,  
76 - this.errorBuilder,  
77 - this.fit = BoxFit.cover,  
78 - required this.onDetect,  
79 - @Deprecated('Use onScannerStarted() instead.') this.onStart,  
80 - this.onScannerStarted,  
81 - this.placeholderBuilder,  
82 - this.scanWindow,  
83 - this.startDelay = false,  
84 - this.overlay,  
85 - super.key,  
86 - }); 64 + /// If this is not null, the barcode scanner will only scan barcodes
  65 + /// which intersect this rectangle.
  66 + ///
  67 + /// This rectangle is relative to the layout size
  68 + /// of the *camera preview widget* in the widget tree,
  69 + /// rather than the actual size of the camera preview output.
  70 + /// This is because the size of the camera preview widget
  71 + /// might not be the same as the size of the camera output.
  72 + ///
  73 + /// For example, the applied [fit] has an effect on the size of the camera preview widget,
  74 + /// while the camera preview size remains the same.
  75 + ///
  76 + /// The following example shows a scan window that is centered,
  77 + /// fills half the height and one third of the width of the layout:
  78 + ///
  79 + /// ```dart
  80 + /// LayoutBuider(
  81 + /// builder: (BuildContext context, BoxConstraints constraints) {
  82 + /// final Size layoutSize = constraints.biggest;
  83 + ///
  84 + /// final double scanWindowWidth = layoutSize.width / 3;
  85 + /// final double scanWindowHeight = layoutSize.height / 2;
  86 + ///
  87 + /// final Rect scanWindow = Rect.fromCenter(
  88 + /// center: layoutSize.center(Offset.zero),
  89 + /// width: scanWindowWidth,
  90 + /// height: scanWindowHeight,
  91 + /// );
  92 + /// }
  93 + /// );
  94 + /// ```
  95 + final Rect? scanWindow;
87 96
88 @override 97 @override
89 State<MobileScanner> createState() => _MobileScannerState(); 98 State<MobileScanner> createState() => _MobileScannerState();
90 } 99 }
91 100
92 -class _MobileScannerState extends State<MobileScanner>  
93 - with WidgetsBindingObserver {  
94 - /// The subscription that listens to barcode detection.  
95 - StreamSubscription<BarcodeCapture>? _barcodesSubscription;  
96 -  
97 - /// The internally managed controller.  
98 - late MobileScannerController _controller;  
99 -  
100 - /// Whether the controller should resume  
101 - /// when the application comes back to the foreground.  
102 - bool _resumeFromBackground = false;  
103 -  
104 - MobileScannerException? _startException;  
105 -  
106 - Widget _buildPlaceholderOrError(BuildContext context, Widget? child) {  
107 - final error = _startException; 101 +class _MobileScannerState extends State<MobileScanner> {
  102 + /// The current scan window.
  103 + Rect? scanWindow;
108 104
109 - if (error != null) {  
110 - return widget.errorBuilder?.call(context, error, child) ??  
111 - const ColoredBox(  
112 - color: Colors.black,  
113 - child: Center(child: Icon(Icons.error, color: Colors.white)), 105 + /// Calculate the scan window based on the given [constraints].
  106 + ///
  107 + /// If the [scanWindow] is already set, this method does nothing.
  108 + void _maybeUpdateScanWindow(
  109 + MobileScannerState scannerState,
  110 + BoxConstraints constraints,
  111 + ) {
  112 + if (widget.scanWindow != null && scanWindow == null) {
  113 + scanWindow = calculateScanWindowRelativeToTextureInPercentage(
  114 + widget.fit,
  115 + widget.scanWindow!,
  116 + textureSize: scannerState.size,
  117 + widgetSize: constraints.biggest,
114 ); 118 );
115 - }  
116 119
117 - return widget.placeholderBuilder?.call(context, child) ??  
118 - const ColoredBox(color: Colors.black); 120 + unawaited(widget.controller.updateScanWindow(scanWindow));
119 } 121 }
120 -  
121 - /// Start the given [scanner].  
122 - Future<void> _startScanner() async {  
123 - if (widget.startDelay) {  
124 - await Future.delayed(const Duration(seconds: 1, milliseconds: 500));  
125 } 122 }
126 123
127 - _barcodesSubscription ??= _controller.barcodes.listen(  
128 - widget.onDetect,  
129 - ); 124 + @override
  125 + Widget build(BuildContext context) {
  126 + return ValueListenableBuilder<MobileScannerState>(
  127 + valueListenable: widget.controller,
  128 + builder: (BuildContext context, MobileScannerState value, Widget? child) {
  129 + if (!value.isInitialized) {
  130 + const Widget defaultPlaceholder = ColoredBox(color: Colors.black);
130 131
131 - if (!_controller.autoStart) {  
132 - debugPrint(  
133 - 'mobile_scanner: not starting automatically because autoStart is set to false in the controller.',  
134 - );  
135 - return; 132 + return widget.placeholderBuilder?.call(context, child) ??
  133 + defaultPlaceholder;
136 } 134 }
137 135
138 - _controller.start().then((arguments) {  
139 - // ignore: deprecated_member_use_from_same_package  
140 - widget.onStart?.call(arguments);  
141 - widget.onScannerStarted?.call(arguments);  
142 - }).catchError((error) {  
143 - if (!mounted) {  
144 - return;  
145 - } 136 + final MobileScannerException? error = value.error;
146 137
147 - if (error is MobileScannerException) {  
148 - _startException = error;  
149 - } else if (error is PlatformException) {  
150 - _startException = MobileScannerException(  
151 - errorCode: MobileScannerErrorCode.genericError,  
152 - errorDetails: MobileScannerErrorDetails(  
153 - code: error.code,  
154 - message: error.message,  
155 - details: error.details,  
156 - ),  
157 - );  
158 - } else {  
159 - _startException = MobileScannerException(  
160 - errorCode: MobileScannerErrorCode.genericError,  
161 - errorDetails: MobileScannerErrorDetails(  
162 - details: error,  
163 - ), 138 + if (error != null) {
  139 + const Widget defaultError = ColoredBox(
  140 + color: Colors.black,
  141 + child: Center(child: Icon(Icons.error, color: Colors.white)),
164 ); 142 );
165 - }  
166 -  
167 - setState(() {});  
168 - });  
169 - }  
170 -  
171 - @override  
172 - void initState() {  
173 - super.initState();  
174 - WidgetsBinding.instance.addObserver(this);  
175 - _controller = widget.controller ?? MobileScannerController();  
176 - _startScanner();  
177 - }  
178 -  
179 - @override  
180 - void didChangeAppLifecycleState(AppLifecycleState state) {  
181 - // App state changed before the controller was initialized.  
182 - if (_controller.isStarting) {  
183 - return;  
184 - }  
185 143
186 - switch (state) {  
187 - case AppLifecycleState.resumed:  
188 - if (_resumeFromBackground) {  
189 - _startScanner();  
190 - }  
191 - case AppLifecycleState.inactive:  
192 - _resumeFromBackground = true;  
193 - _controller.stop();  
194 - default:  
195 - break;  
196 - } 144 + return widget.errorBuilder?.call(context, error, child) ??
  145 + defaultError;
197 } 146 }
198 147
199 - Rect? scanWindow;  
200 -  
201 - @override  
202 - Widget build(BuildContext context) {  
203 return LayoutBuilder( 148 return LayoutBuilder(
204 builder: (context, constraints) { 149 builder: (context, constraints) {
205 - return ValueListenableBuilder<MobileScannerArguments?>(  
206 - valueListenable: _controller.startArguments,  
207 - builder: (context, value, child) {  
208 - if (value == null) {  
209 - return _buildPlaceholderOrError(context, child);  
210 - } 150 + _maybeUpdateScanWindow(value, constraints);
211 151
212 - if (widget.scanWindow != null && scanWindow == null) {  
213 - scanWindow = calculateScanWindowRelativeToTextureInPercentage(  
214 - widget.fit,  
215 - widget.scanWindow!,  
216 - textureSize: value.size,  
217 - widgetSize: constraints.biggest, 152 + final Widget? overlay =
  153 + widget.overlayBuilder?.call(context, constraints);
  154 + final Size cameraPreviewSize = value.size;
  155 +
  156 + final Widget scannerWidget = ClipRect(
  157 + child: SizedBox.fromSize(
  158 + size: constraints.biggest,
  159 + child: FittedBox(
  160 + fit: widget.fit,
  161 + child: SizedBox(
  162 + width: cameraPreviewSize.width,
  163 + height: cameraPreviewSize.height,
  164 + child: MobileScannerPlatform.instance.buildCameraView(),
  165 + ),
  166 + ),
  167 + ),
218 ); 168 );
219 169
220 - _controller.updateScanWindow(scanWindow); 170 + if (overlay == null) {
  171 + return scannerWidget;
221 } 172 }
222 - if (widget.overlay != null) { 173 +
223 return Stack( 174 return Stack(
224 alignment: Alignment.center, 175 alignment: Alignment.center,
225 - children: [  
226 - _scanner(  
227 - value.size,  
228 - value.webId,  
229 - value.textureId,  
230 - value.numberOfCameras,  
231 - ),  
232 - widget.overlay!, 176 + children: <Widget>[
  177 + scannerWidget,
  178 + overlay,
233 ], 179 ],
234 ); 180 );
235 - } else {  
236 - return _scanner(  
237 - value.size,  
238 - value.webId,  
239 - value.textureId,  
240 - value.numberOfCameras,  
241 - );  
242 - }  
243 - },  
244 - );  
245 }, 181 },
246 ); 182 );
247 - }  
248 -  
249 - Widget _scanner(  
250 - Size size,  
251 - String? webId,  
252 - int? textureId,  
253 - int? numberOfCameras,  
254 - ) {  
255 - return ClipRect(  
256 - child: LayoutBuilder(  
257 - builder: (_, constraints) {  
258 - return SizedBox.fromSize(  
259 - size: constraints.biggest,  
260 - child: FittedBox(  
261 - fit: widget.fit,  
262 - child: SizedBox(  
263 - width: size.width,  
264 - height: size.height,  
265 - child: kIsWeb  
266 - ? HtmlElementView(viewType: webId!)  
267 - : Texture(textureId: textureId!),  
268 - ),  
269 - ),  
270 - );  
271 }, 183 },
272 - ),  
273 ); 184 );
274 } 185 }
275 186
276 @override 187 @override
277 void dispose() { 188 void dispose() {
278 - _controller.updateScanWindow(null);  
279 - WidgetsBinding.instance.removeObserver(this);  
280 - _barcodesSubscription?.cancel();  
281 - _barcodesSubscription = null;  
282 - _controller.dispose();  
283 super.dispose(); 189 super.dispose();
  190 + // When this widget is unmounted, reset the scan window.
  191 + unawaited(widget.controller.updateScanWindow(null));
284 } 192 }
285 } 193 }
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';  
7 -import 'package:flutter/services.dart'; 3 +import 'package:flutter/widgets.dart';
8 import 'package:mobile_scanner/src/enums/barcode_format.dart'; 4 import 'package:mobile_scanner/src/enums/barcode_format.dart';
9 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 5 import 'package:mobile_scanner/src/enums/camera_facing.dart';
10 import 'package:mobile_scanner/src/enums/detection_speed.dart'; 6 import 'package:mobile_scanner/src/enums/detection_speed.dart';
11 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 7 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
12 -import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart';  
13 import 'package:mobile_scanner/src/enums/torch_state.dart'; 8 import 'package:mobile_scanner/src/enums/torch_state.dart';
14 import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; 9 import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
15 -import 'package:mobile_scanner/src/objects/barcode.dart'; 10 +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
  11 +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
16 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 12 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
17 -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; 13 +import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
  14 +import 'package:mobile_scanner/src/objects/start_options.dart';
18 15
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 { 16 +/// The controller for the [MobileScanner] widget.
  17 +class MobileScannerController extends ValueNotifier<MobileScannerState> {
  18 + /// Construct a new [MobileScannerController] instance.
22 MobileScannerController({ 19 MobileScannerController({
23 - this.facing = CameraFacing.back, 20 + this.cameraResolution,
24 this.detectionSpeed = DetectionSpeed.normal, 21 this.detectionSpeed = DetectionSpeed.normal,
25 - this.detectionTimeoutMs = 250,  
26 - this.torchEnabled = false,  
27 - this.formats, 22 + int detectionTimeoutMs = 250,
  23 + this.facing = CameraFacing.back,
  24 + this.formats = const <BarcodeFormat>[],
28 this.returnImage = false, 25 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, 26 + this.torchEnabled = false,
35 this.useNewCameraSelector = false, 27 this.useNewCameraSelector = false,
36 - }); 28 + }) : detectionTimeoutMs =
  29 + detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0,
  30 + assert(
  31 + detectionTimeoutMs >= 0,
  32 + 'The detection timeout must be greater than or equal to 0.',
  33 + ),
  34 + super(MobileScannerState.uninitialized(facing));
37 35
38 - /// Select which camera should be used. 36 + /// The desired resolution for the camera.
39 /// 37 ///
40 - /// Default: CameraFacing.back  
41 - final CameraFacing facing;  
42 -  
43 - /// Enable or disable the torch (Flash) on start 38 + /// When this value is provided, the camera will try to match this resolution,
  39 + /// or fallback to the closest available resolution.
  40 + /// When this is null, Android defaults to a resolution of 640x480.
44 /// 41 ///
45 - /// Default: disabled  
46 - final bool torchEnabled;  
47 -  
48 - /// Set to true if you want to return the image buffer with the Barcode event 42 + /// Bear in mind that changing the resolution has an effect on the aspect ratio.
49 /// 43 ///
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; 44 + /// When the camera orientation changes,
  45 + /// the resolution will be flipped to match the new dimensions of the display.
  46 + ///
  47 + /// Currently only supported on Android.
  48 + final Size? cameraResolution;
55 49
56 - /// Sets the speed of detections. 50 + /// The detection speed for the scanner.
57 /// 51 ///
58 - /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices 52 + /// Defaults to [DetectionSpeed.normal].
59 final DetectionSpeed detectionSpeed; 53 final DetectionSpeed detectionSpeed;
60 54
61 - /// Sets the timeout, in milliseconds, of the scanner. 55 + /// The detection timeout, in milliseconds, for the scanner.
62 /// 56 ///
63 /// This timeout is ignored if the [detectionSpeed] 57 /// This timeout is ignored if the [detectionSpeed]
64 /// is not set to [DetectionSpeed.normal]. 58 /// is not set to [DetectionSpeed.normal].
@@ -67,439 +61,342 @@ class MobileScannerController { @@ -67,439 +61,342 @@ class MobileScannerController {
67 /// which prevents memory issues on older devices. 61 /// which prevents memory issues on older devices.
68 final int detectionTimeoutMs; 62 final int detectionTimeoutMs;
69 63
70 - /// Automatically start the mobileScanner on initialization.  
71 - final bool autoStart; 64 + /// The facing direction for the camera.
  65 + ///
  66 + /// Defaults to the back-facing camera.
  67 + final CameraFacing facing;
72 68
73 - /// The desired resolution for the camera. 69 + /// The formats that the scanner should detect.
74 /// 70 ///
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. 71 + /// If this is empty, all supported formats are detected.
  72 + final List<BarcodeFormat> formats;
  73 +
  74 + /// Whether scanned barcodes should contain the image
  75 + /// that is embedded into the barcode.
78 /// 76 ///
79 - /// Bear in mind that changing the resolution has an effect on the aspect ratio. 77 + /// If this is false, [BarcodeCapture.image] will always be null.
80 /// 78 ///
81 - /// When the camera orientation changes,  
82 - /// the resolution will be flipped to match the new dimensions of the display. 79 + /// Defaults to false, and is only supported on iOS and Android.
  80 + final bool returnImage;
  81 +
  82 + /// Whether the flashlight should be turned on when the camera is started.
83 /// 83 ///
84 - /// Currently only supported on Android.  
85 - final Size? cameraResolution; 84 + /// Defaults to false.
  85 + final bool torchEnabled;
86 86
87 - /// Use the new resolution selector. Warning: not fully tested, may produce  
88 - /// unwanted/zoomed images. 87 + /// Use the new resolution selector.
  88 + ///
  89 + /// This feature is experimental and not fully tested yet.
  90 + /// Use caution when using this flag,
  91 + /// as the new resolution selector may produce unwanted or zoomed images.
89 /// 92 ///
90 - /// Only supported on Android 93 + /// Only supported on Android.
91 final bool useNewCameraSelector; 94 final bool useNewCameraSelector;
92 95
93 - /// Sets the barcode stream 96 + /// The internal barcode controller, that listens for detected barcodes.
94 final StreamController<BarcodeCapture> _barcodesController = 97 final StreamController<BarcodeCapture> _barcodesController =
95 StreamController.broadcast(); 98 StreamController.broadcast();
96 99
  100 + /// Get the stream of scanned barcodes.
97 Stream<BarcodeCapture> get barcodes => _barcodesController.stream; 101 Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
98 102
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 + StreamSubscription<BarcodeCapture?>? _barcodesSubscription;
  104 + StreamSubscription<TorchState>? _torchStateSubscription;
  105 + StreamSubscription<double>? _zoomScaleSubscription;
103 106
104 - @Deprecated(  
105 - 'Instead, use the result of calling `start()` to determine if permissions were granted.',  
106 - )  
107 - Function(bool permissionGranted)? onPermissionSet; 107 + bool _isDisposed = false;
108 108
109 - /// Listen to events from the platform specific code  
110 - StreamSubscription? events; 109 + void _disposeListeners() {
  110 + _barcodesSubscription?.cancel();
  111 + _torchStateSubscription?.cancel();
  112 + _zoomScaleSubscription?.cancel();
111 113
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); 114 + _barcodesSubscription = null;
  115 + _torchStateSubscription = null;
  116 + _zoomScaleSubscription = null;
  117 + }
118 118
119 - /// A notifier that provides the state of which camera is being used  
120 - late final ValueNotifier<CameraFacing> cameraFacingState =  
121 - ValueNotifier(facing); 119 + void _setupListeners() {
  120 + _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream
  121 + .listen((BarcodeCapture? barcode) {
  122 + if (_barcodesController.isClosed || barcode == null) {
  123 + return;
  124 + }
122 125
123 - /// A notifier that provides zoomScale.  
124 - final ValueNotifier<double> zoomScaleState = ValueNotifier(0.0); 126 + _barcodesController.add(barcode);
  127 + });
125 128
126 - bool isStarting = false; 129 + _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream
  130 + .listen((TorchState torchState) {
  131 + if (_isDisposed) {
  132 + return;
  133 + }
127 134
128 - /// A notifier that provides availability of the Torch (Flash)  
129 - final ValueNotifier<bool?> hasTorchState = ValueNotifier(false); 135 + value = value.copyWith(torchState: torchState);
  136 + });
130 137
131 - /// Returns whether the device has a torch.  
132 - ///  
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 - ); 138 + _zoomScaleSubscription = MobileScannerPlatform.instance.zoomScaleStateStream
  139 + .listen((double zoomScale) {
  140 + if (_isDisposed) {
  141 + return;
140 } 142 }
141 143
142 - return hasTorch; 144 + value = value.copyWith(zoomScale: zoomScale);
  145 + });
143 } 146 }
144 147
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 - } 148 + void _throwIfNotInitialized() {
  149 + if (!value.isInitialized) {
  150 + throw const MobileScannerException(
  151 + errorCode: MobileScannerErrorCode.controllerUninitialized,
  152 + errorDetails: MobileScannerErrorDetails(
  153 + message: 'The MobileScannerController has not been initialized.',
  154 + ),
  155 + );
170 } 156 }
171 157
172 - if (cameraResolution != null) {  
173 - arguments['cameraResolution'] = <int>[  
174 - cameraResolution!.width.toInt(),  
175 - cameraResolution!.height.toInt(),  
176 - ]; 158 + if (_isDisposed) {
  159 + throw const MobileScannerException(
  160 + errorCode: MobileScannerErrorCode.controllerDisposed,
  161 + errorDetails: MobileScannerErrorDetails(
  162 + message:
  163 + 'The MobileScannerController was used after it has been disposed.',
  164 + ),
  165 + );
177 } 166 }
178 -  
179 - return arguments;  
180 } 167 }
181 168
182 - /// Start scanning for barcodes.  
183 - /// Upon calling this method, the necessary camera permission will be requested. 169 + /// Analyze an image file.
  170 + ///
  171 + /// The [path] points to a file on the device.
184 /// 172 ///
185 - /// Returns an instance of [MobileScannerArguments]  
186 - /// when the scanner was successfully started.  
187 - /// Returns null if the scanner is currently starting. 173 + /// This is only supported on Android and iOS.
188 /// 174 ///
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; 175 + /// Returns the [BarcodeCapture] that was found in the image.
  176 + Future<BarcodeCapture?> analyzeImage(String path) {
  177 + return MobileScannerPlatform.instance.analyzeImage(path);
196 } 178 }
197 179
198 - events ??= _eventChannel  
199 - .receiveBroadcastStream()  
200 - .listen((data) => _handleEvent(data as Map)); 180 + /// Build a camera preview widget.
  181 + Widget buildCameraView() {
  182 + _throwIfNotInitialized();
201 183
202 - isStarting = true; 184 + return MobileScannerPlatform.instance.buildCameraView();
  185 + }
203 186
204 - // Check authorization status  
205 - if (!kIsWeb) {  
206 - final MobileScannerState state; 187 + /// Reset the zoom scale of the camera.
  188 + ///
  189 + /// Does nothing if the camera is not running.
  190 + Future<void> resetZoomScale() async {
  191 + _throwIfNotInitialized();
207 192
208 - try {  
209 - state = MobileScannerState.fromRawValue(  
210 - await _methodChannel.invokeMethod('state') as int? ?? 0,  
211 - );  
212 - } on PlatformException catch (error) {  
213 - isStarting = false; 193 + if (!value.isRunning) {
  194 + return;
  195 + }
214 196
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 - ); 197 + // When the platform has updated the zoom scale,
  198 + // it will send an update through the zoom scale state event stream.
  199 + await MobileScannerPlatform.instance.resetZoomScale();
223 } 200 }
224 201
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; 202 + /// Set the zoom scale of the camera.
  203 + ///
  204 + /// The [zoomScale] must be between 0.0 and 1.0 (both inclusive).
  205 + ///
  206 + /// If the [zoomScale] is out of range,
  207 + /// it is adjusted to fit within the allowed range.
  208 + ///
  209 + /// Does nothing if the camera is not running.
  210 + Future<void> setZoomScale(double zoomScale) async {
  211 + _throwIfNotInitialized();
233 212
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 - ); 213 + if (!value.isRunning) {
  214 + return;
250 } 215 }
251 216
252 - case MobileScannerState.authorized:  
253 - break;  
254 - }  
255 - } 217 + final double clampedZoomScale = zoomScale.clamp(0.0, 1.0);
256 218
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; 219 + // Update the zoom scale state to the new state.
  220 + // When the platform has updated the zoom scale,
  221 + // it will send an update through the zoom scale state event stream.
  222 + await MobileScannerPlatform.instance.setZoomScale(clampedZoomScale);
279 } 223 }
280 - }  
281 -  
282 - isStarting = false;  
283 224
284 - throw MobileScannerException(  
285 - errorCode: errorCode, 225 + /// Start scanning for barcodes.
  226 + /// Upon calling this method, the necessary camera permission will be requested.
  227 + ///
  228 + /// The [cameraDirection] can be used to specify the camera direction.
  229 + /// If this is null, this defaults to the [facing] value.
  230 + ///
  231 + /// Does nothing if the camera is already running.
  232 + Future<void> start({CameraFacing? cameraDirection}) async {
  233 + if (_isDisposed) {
  234 + throw const MobileScannerException(
  235 + errorCode: MobileScannerErrorCode.controllerDisposed,
286 errorDetails: MobileScannerErrorDetails( 236 errorDetails: MobileScannerErrorDetails(
287 - code: error.code,  
288 - details: error.details as Object?,  
289 - message: error.message, 237 + message:
  238 + 'The MobileScannerController was used after it has been disposed.',
290 ), 239 ),
291 ); 240 );
292 } 241 }
293 242
294 - if (startResult == null) {  
295 - isStarting = false;  
296 - throw const MobileScannerException(  
297 - errorCode: MobileScannerErrorCode.genericError,  
298 - ); 243 + // Do nothing if the camera is already running.
  244 + if (value.isRunning) {
  245 + return;
299 } 246 }
300 247
301 - final hasTorch = startResult['torchable'] as bool? ?? false;  
302 - hasTorchState.value = hasTorch; 248 + final CameraFacing effectiveDirection = cameraDirection ?? facing;
303 249
304 - final Size size;  
305 -  
306 - if (kIsWeb) {  
307 - size = Size(  
308 - startResult['videoWidth'] as double? ?? 0,  
309 - startResult['videoHeight'] as double? ?? 0, 250 + final StartOptions options = StartOptions(
  251 + cameraDirection: effectiveDirection,
  252 + cameraResolution: cameraResolution,
  253 + detectionSpeed: detectionSpeed,
  254 + detectionTimeoutMs: detectionTimeoutMs,
  255 + formats: formats,
  256 + returnImage: returnImage,
  257 + torchEnabled: torchEnabled,
310 ); 258 );
311 - } else {  
312 - final Map<Object?, Object?>? sizeInfo =  
313 - startResult['size'] as Map<Object?, Object?>?;  
314 259
315 - size = Size(  
316 - sizeInfo?['width'] as double? ?? 0,  
317 - sizeInfo?['height'] as double? ?? 0, 260 + try {
  261 + _setupListeners();
  262 +
  263 + final MobileScannerViewAttributes viewAttributes =
  264 + await MobileScannerPlatform.instance.start(
  265 + options,
318 ); 266 );
319 - }  
320 267
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, 268 + value = value.copyWith(
  269 + availableCameras: viewAttributes.numberOfCameras,
  270 + cameraDirection: effectiveDirection,
  271 + isInitialized: true,
  272 + isRunning: true,
  273 + size: viewAttributes.size,
  274 + // If the device has a flashlight, let the platform update the torch state.
  275 + // If it does not have one, provide the unavailable state directly.
  276 + torchState: viewAttributes.hasTorch ? null : TorchState.unavailable,
  277 + );
  278 + } on MobileScannerException catch (error) {
  279 + // The initialization finished with an error.
  280 + // To avoid stale values, reset the output size,
  281 + // torch state and zoom scale to the defaults.
  282 + if (!_isDisposed) {
  283 + value = value.copyWith(
  284 + cameraDirection: facing,
  285 + isInitialized: true,
  286 + isRunning: false,
  287 + error: error,
  288 + size: Size.zero,
  289 + torchState: TorchState.unavailable,
  290 + zoomScale: 1.0,
328 ); 291 );
329 } 292 }
  293 + } on PermissionRequestPendingException catch (_) {
  294 + // If a permission request was already pending, do nothing.
  295 + }
  296 + }
330 297
331 - /// Stops the camera, but does not dispose this controller. 298 + /// Stop the camera.
  299 + ///
  300 + /// After calling this method, the camera can be restarted using [start].
  301 + ///
  302 + /// Does nothing if the camera is already stopped.
332 Future<void> stop() async { 303 Future<void> stop() async {
333 - await _methodChannel.invokeMethod('stop'); 304 + // Do nothing if not initialized or already stopped.
  305 + // On the web, the permission popup triggers a lifecycle change from resumed to inactive,
  306 + // due to the permission popup gaining focus.
  307 + // This would 'stop' the camera while it is not ready yet.
  308 + if (!value.isInitialized || !value.isRunning || _isDisposed) {
  309 + return;
  310 + }
  311 +
  312 + _disposeListeners();
334 313
335 // After the camera stopped, set the torch state to off, 314 // After the camera stopped, set the torch state to off,
336 // as the torch state callback is never called when the camera is stopped. 315 // as the torch state callback is never called when the camera is stopped.
337 - torchState.value = TorchState.off; 316 + value = value.copyWith(
  317 + isRunning: false,
  318 + torchState: TorchState.off,
  319 + );
  320 +
  321 + await MobileScannerPlatform.instance.stop();
338 } 322 }
339 323
340 - /// Switches the torch on or off.  
341 - ///  
342 - /// Does nothing if the device has no torch. 324 + /// Switch between the front and back camera.
343 /// 325 ///
344 - /// Throws if the controller was not initialized.  
345 - Future<void> toggleTorch() async {  
346 - final hasTorch = hasTorchState.value; 326 + /// Does nothing if the device has less than 2 cameras.
  327 + Future<void> switchCamera() async {
  328 + _throwIfNotInitialized();
347 329
348 - if (hasTorch == null) {  
349 - throw const MobileScannerException(  
350 - errorCode: MobileScannerErrorCode.controllerUninitialized,  
351 - );  
352 - } 330 + final int? availableCameras = value.availableCameras;
353 331
354 - if (!hasTorch) { 332 + // Do nothing if the amount of cameras is less than 2 cameras.
  333 + // If the the current platform does not provide the amount of cameras,
  334 + // continue anyway.
  335 + if (availableCameras != null && availableCameras < 2) {
355 return; 336 return;
356 } 337 }
357 338
358 - final TorchState newState =  
359 - torchState.value == TorchState.off ? TorchState.on : TorchState.off; 339 + await stop();
360 340
361 - await _methodChannel.invokeMethod('torch', newState.rawValue);  
362 - } 341 + final CameraFacing cameraDirection = value.cameraDirection;
363 342
364 - /// Changes the state of the camera (front or back).  
365 - ///  
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); 343 + await start(
  344 + cameraDirection: cameraDirection == CameraFacing.front
  345 + ? CameraFacing.back
  346 + : CameraFacing.front,
  347 + );
374 } 348 }
375 349
376 - /// Handles a local image file.  
377 - /// Returns true if a barcode or QR code is found.  
378 - /// Returns false if nothing is found. 350 + /// Switches the flashlight on or off.
379 /// 351 ///
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 - } 352 + /// Does nothing if the device has no torch,
  353 + /// or if the camera is not running.
  354 + Future<void> toggleTorch() async {
  355 + _throwIfNotInitialized();
390 356
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 - ); 357 + if (!value.isRunning) {
  358 + return;
403 } 359 }
404 - await _methodChannel.invokeMethod('setScale', zoomScale); 360 +
  361 + final TorchState torchState = value.torchState;
  362 +
  363 + if (torchState == TorchState.unavailable) {
  364 + return;
405 } 365 }
406 366
407 - /// Reset the zoomScale of the camera to use standard scale 1x.  
408 - Future<void> resetZoomScale() async {  
409 - await _methodChannel.invokeMethod('resetScale'); 367 + final TorchState newState =
  368 + torchState == TorchState.off ? TorchState.on : TorchState.off;
  369 +
  370 + // Update the torch state to the new state.
  371 + // When the platform has updated the torch state,
  372 + // it will send an update through the torch state event stream.
  373 + await MobileScannerPlatform.instance.setTorchState(newState);
410 } 374 }
411 375
412 - /// Disposes the MobileScannerController and closes all listeners. 376 + /// Update the scan window with the given [window] rectangle.
413 /// 377 ///
414 - /// If you call this, you cannot use this controller object anymore.  
415 - void dispose() {  
416 - stop();  
417 - events?.cancel();  
418 - _barcodesController.close(); 378 + /// If [window] is null, the scan window will be reset to the full camera preview.
  379 + Future<void> updateScanWindow(Rect? window) async {
  380 + if (_isDisposed || !value.isInitialized) {
  381 + return;
419 } 382 }
420 383
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 - _barcodesController.add(  
438 - BarcodeCapture(  
439 - raw: data,  
440 - barcodes: parsed,  
441 - image: event['image'] as Uint8List?,  
442 - width: event['width'] as double?,  
443 - height: event['height'] as double?,  
444 - ),  
445 - );  
446 - case 'barcodeMac':  
447 - _barcodesController.add(  
448 - BarcodeCapture(  
449 - raw: data,  
450 - barcodes: [  
451 - Barcode(  
452 - rawValue: (data as Map)['payload'] as String?,  
453 - format: BarcodeFormat.fromRawValue(  
454 - data['symbology'] as int? ?? -1,  
455 - ),  
456 - ),  
457 - ],  
458 - ),  
459 - );  
460 - case 'barcodeWeb':  
461 - final barcode = data as Map?;  
462 - final corners = barcode?['corners'] as List<Object?>? ?? <Object?>[];  
463 -  
464 - _barcodesController.add(  
465 - BarcodeCapture(  
466 - raw: data,  
467 - barcodes: [  
468 - if (barcode != null)  
469 - Barcode(  
470 - rawValue: barcode['rawValue'] as String?,  
471 - rawBytes: barcode['rawBytes'] as Uint8List?,  
472 - format: BarcodeFormat.fromRawValue(  
473 - barcode['format'] as int? ?? -1,  
474 - ),  
475 - corners: List.unmodifiable(  
476 - corners.cast<Map<Object?, Object?>>().map(  
477 - (Map<Object?, Object?> e) {  
478 - return Offset(e['x']! as double, e['y']! as double);  
479 - },  
480 - ),  
481 - ),  
482 - ),  
483 - ],  
484 - ),  
485 - );  
486 - case 'error':  
487 - throw MobileScannerException(  
488 - errorCode: MobileScannerErrorCode.genericError,  
489 - errorDetails: MobileScannerErrorDetails(message: data as String?),  
490 - );  
491 - default:  
492 - throw UnimplementedError(name as String?);  
493 - } 384 + await MobileScannerPlatform.instance.updateScanWindow(window);
494 } 385 }
495 386
496 - /// updates the native ScanWindow  
497 - Future<void> updateScanWindow(Rect? window) async {  
498 - List? data;  
499 - if (window != null) {  
500 - data = [window.left, window.top, window.right, window.bottom]; 387 + /// Dispose the controller.
  388 + ///
  389 + /// Once the controller is disposed, it cannot be used anymore.
  390 + @override
  391 + Future<void> dispose() async {
  392 + if (_isDisposed) {
  393 + return;
501 } 394 }
502 395
503 - await _methodChannel.invokeMethod('updateScanWindow', {'rect': data}); 396 + _isDisposed = true;
  397 + unawaited(_barcodesController.close());
  398 + super.dispose();
  399 +
  400 + await MobileScannerPlatform.instance.dispose();
504 } 401 }
505 } 402 }
@@ -39,3 +39,10 @@ class MobileScannerErrorDetails { @@ -39,3 +39,10 @@ class MobileScannerErrorDetails {
39 /// The error message from the [PlatformException]. 39 /// The error message from the [PlatformException].
40 final String? message; 40 final String? message;
41 } 41 }
  42 +
  43 +/// This class represents an exception that is thrown
  44 +/// when the scanner was (re)started while a permission request was pending.
  45 +///
  46 +/// This exception type is only used internally,
  47 +/// and is not part of the public API.
  48 +class PermissionRequestPendingException implements Exception {}
  1 +import 'package:flutter/widgets.dart';
  2 +import 'package:mobile_scanner/src/enums/torch_state.dart';
  3 +import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart';
  4 +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
  5 +import 'package:mobile_scanner/src/objects/barcode_capture.dart';
  6 +import 'package:mobile_scanner/src/objects/start_options.dart';
  7 +import 'package:plugin_platform_interface/plugin_platform_interface.dart';
  8 +
  9 +/// The platform interface for the `mobile_scanner` plugin.
  10 +abstract class MobileScannerPlatform extends PlatformInterface {
  11 + /// Constructs a MobileScannerPlatform.
  12 + MobileScannerPlatform() : super(token: _token);
  13 +
  14 + static final Object _token = Object();
  15 +
  16 + static MobileScannerPlatform _instance = MethodChannelMobileScanner();
  17 +
  18 + /// The default instance of [MobileScannerPlatform] to use.
  19 + ///
  20 + /// Defaults to [MethodChannelMobileScanner].
  21 + static MobileScannerPlatform get instance => _instance;
  22 +
  23 + /// Platform-specific implementations should set this with their own
  24 + /// platform-specific class that extends [MobileScannerPlatform] when
  25 + /// they register themselves.
  26 + static set instance(MobileScannerPlatform instance) {
  27 + PlatformInterface.verifyToken(instance, _token);
  28 + _instance = instance;
  29 + }
  30 +
  31 + /// Get the stream of barcode captures.
  32 + Stream<BarcodeCapture?> get barcodesStream {
  33 + throw UnimplementedError('barcodesStream has not been implemented.');
  34 + }
  35 +
  36 + /// Get the stream of torch state changes.
  37 + Stream<TorchState> get torchStateStream {
  38 + throw UnimplementedError('torchStateStream has not been implemented.');
  39 + }
  40 +
  41 + /// Get the stream of zoom scale changes.
  42 + Stream<double> get zoomScaleStateStream {
  43 + throw UnimplementedError('zoomScaleStateStream has not been implemented.');
  44 + }
  45 +
  46 + /// Analyze a local image file for barcodes.
  47 + ///
  48 + /// The [path] is the path to the file on disk.
  49 + ///
  50 + /// Returns the barcodes that were found in the image.
  51 + Future<BarcodeCapture?> analyzeImage(String path) {
  52 + throw UnimplementedError('analyzeImage() has not been implemented.');
  53 + }
  54 +
  55 + /// Build the camera view for the barcode scanner.
  56 + Widget buildCameraView() {
  57 + throw UnimplementedError('buildCameraView() has not been implemented.');
  58 + }
  59 +
  60 + /// Reset the zoom scale, so that the camera is fully zoomed out.
  61 + Future<void> resetZoomScale() {
  62 + throw UnimplementedError('resetZoomScale() has not been implemented.');
  63 + }
  64 +
  65 + /// Set the source url for the barcode library.
  66 + ///
  67 + /// This is only supported on the web.
  68 + void setBarcodeLibraryScriptUrl(String scriptUrl) {}
  69 +
  70 + /// Set the torch state of the active camera.
  71 + Future<void> setTorchState(TorchState torchState) {
  72 + throw UnimplementedError('setTorchState() has not been implemented.');
  73 + }
  74 +
  75 + /// Set the zoom scale of the camera.
  76 + ///
  77 + /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive).
  78 + /// A value of `0.0` indicates that the camera is fully zoomed out,
  79 + /// while `1.0` indicates that the camera is fully zoomed in.
  80 + Future<void> setZoomScale(double zoomScale) {
  81 + throw UnimplementedError('setZoomScale() has not been implemented.');
  82 + }
  83 +
  84 + /// Start the barcode scanner and prepare a scanner view.
  85 + ///
  86 + /// Upon calling this method, the necessary camera permission will be requested.
  87 + ///
  88 + /// The given [StartOptions.cameraDirection] is used as the direction for the camera that needs to be set up.
  89 + Future<MobileScannerViewAttributes> start(StartOptions startOptions) {
  90 + throw UnimplementedError('start() has not been implemented.');
  91 + }
  92 +
  93 + /// Stop the camera.
  94 + Future<void> stop() {
  95 + throw UnimplementedError('stop() has not been implemented.');
  96 + }
  97 +
  98 + /// Update the scan window to the given [window] rectangle.
  99 + ///
  100 + /// Any barcodes that do not intersect with the given [window] will be ignored.
  101 + ///
  102 + /// If [window] is `null`, the scan window will be reset to the full screen.
  103 + Future<void> updateScanWindow(Rect? window) {
  104 + throw UnimplementedError('updateScanWindow() has not been implemented.');
  105 + }
  106 +
  107 + /// Dispose of this [MobileScannerPlatform] instance.
  108 + Future<void> dispose() {
  109 + throw UnimplementedError('dispose() has not been implemented.');
  110 + }
  111 +}
  1 +import 'dart:ui';
  2 +
  3 +/// This class defines the attributes for the mobile scanner view.
  4 +class MobileScannerViewAttributes {
  5 + const MobileScannerViewAttributes({
  6 + required this.hasTorch,
  7 + this.numberOfCameras,
  8 + required this.size,
  9 + });
  10 +
  11 + /// Whether the current active camera has a torch.
  12 + final bool hasTorch;
  13 +
  14 + /// The number of available cameras.
  15 + final int? numberOfCameras;
  16 +
  17 + /// The size of the camera output.
  18 + final Size size;
  19 +}
@@ -147,7 +147,9 @@ class Barcode { @@ -147,7 +147,9 @@ class Barcode {
147 /// The SMS message that is embedded in the barcode. 147 /// The SMS message that is embedded in the barcode.
148 final SMS? sms; 148 final SMS? sms;
149 149
150 - /// The type of the [format] of the barcode. 150 + /// The contextual type of the [format] of the barcode.
  151 + ///
  152 + /// For example: TYPE_TEXT, TYPE_PRODUCT, TYPE_URL, etc.
151 /// 153 ///
152 /// For types that are recognized, 154 /// For types that are recognized,
153 /// but could not be parsed correctly, [BarcodeType.text] will be returned. 155 /// but could not be parsed correctly, [BarcodeType.text] will be returned.
@@ -6,38 +6,25 @@ import 'package:mobile_scanner/src/objects/barcode.dart'; @@ -6,38 +6,25 @@ import 'package:mobile_scanner/src/objects/barcode.dart';
6 /// This class represents a scanned barcode. 6 /// This class represents a scanned barcode.
7 class BarcodeCapture { 7 class BarcodeCapture {
8 /// Create a new [BarcodeCapture] instance. 8 /// Create a new [BarcodeCapture] instance.
9 - BarcodeCapture({ 9 + const BarcodeCapture({
10 this.barcodes = const <Barcode>[], 10 this.barcodes = const <Barcode>[],
11 - double? height,  
12 this.image, 11 this.image,
13 this.raw, 12 this.raw,
14 - double? width,  
15 - }) : size =  
16 - width == null && height == null ? Size.zero : Size(width!, height!); 13 + this.size = Size.zero,
  14 + });
17 15
18 /// The list of scanned barcodes. 16 /// The list of scanned barcodes.
19 final List<Barcode> barcodes; 17 final List<Barcode> barcodes;
20 18
21 /// The bytes of the image that is embedded in the barcode. 19 /// The bytes of the image that is embedded in the barcode.
22 /// 20 ///
23 - /// This null if [MobileScannerController.returnImage] is false. 21 + /// This null if [MobileScannerController.returnImage] is false,
  22 + /// or if there is no available image.
24 final Uint8List? image; 23 final Uint8List? image;
25 24
26 /// The raw data of the scanned barcode. 25 /// The raw data of the scanned barcode.
27 - final dynamic raw; // TODO: this should be `Object?` instead of dynamic 26 + final Object? raw;
28 27
29 /// The size of the scanned barcode. 28 /// The size of the scanned barcode.
30 final Size size; 29 final Size size;
31 -  
32 - /// The width of the scanned barcode.  
33 - ///  
34 - /// Prefer using `size.width` instead,  
35 - /// as this getter will be removed in the future.  
36 - double get width => size.width;  
37 -  
38 - /// The height of the scanned barcode.  
39 - ///  
40 - /// Prefer using `size.height` instead,  
41 - /// as this getter will be removed in the future.  
42 - double get height => size.height;  
43 } 30 }
1 -import 'package:flutter/material.dart';  
2 -  
3 -/// The start arguments of the scanner.  
4 -class MobileScannerArguments {  
5 - /// The output size of the camera.  
6 - /// This value can be used to draw a box in the image.  
7 - final Size size;  
8 -  
9 - /// A bool which is true if the device has a torch.  
10 - final bool hasTorch;  
11 -  
12 - /// The texture id of the capture used internally.  
13 - final int? textureId;  
14 -  
15 - /// The texture id of the capture used internally if device is web.  
16 - final String? webId;  
17 -  
18 - /// Indicates how many cameras are available.  
19 - ///  
20 - /// Currently only supported on Android.  
21 - final int? numberOfCameras;  
22 -  
23 - MobileScannerArguments({  
24 - required this.size,  
25 - required this.hasTorch,  
26 - this.textureId,  
27 - this.webId,  
28 - this.numberOfCameras,  
29 - });  
30 -}  
  1 +import 'dart:ui';
  2 +
  3 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
  4 +import 'package:mobile_scanner/src/enums/torch_state.dart';
  5 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
  6 +
  7 +/// This class represents the current state of a [MobileScannerController].
  8 +class MobileScannerState {
  9 + /// Create a new [MobileScannerState] instance.
  10 + const MobileScannerState({
  11 + required this.availableCameras,
  12 + required this.cameraDirection,
  13 + required this.isInitialized,
  14 + required this.isRunning,
  15 + required this.size,
  16 + required this.torchState,
  17 + required this.zoomScale,
  18 + this.error,
  19 + });
  20 +
  21 + /// Create a new [MobileScannerState] instance that is uninitialized.
  22 + const MobileScannerState.uninitialized(CameraFacing facing)
  23 + : this(
  24 + availableCameras: null,
  25 + cameraDirection: facing,
  26 + isInitialized: false,
  27 + isRunning: false,
  28 + size: Size.zero,
  29 + torchState: TorchState.unavailable,
  30 + zoomScale: 1.0,
  31 + );
  32 +
  33 + /// The number of available cameras.
  34 + ///
  35 + /// This is null if the number of cameras is unknown.
  36 + final int? availableCameras;
  37 +
  38 + /// The facing direction of the camera.
  39 + final CameraFacing cameraDirection;
  40 +
  41 + /// The error that occurred while setting up or using the canera.
  42 + final MobileScannerException? error;
  43 +
  44 + /// Whether the mobile scanner has initialized successfully.
  45 + ///
  46 + /// This is `true` if the camera is ready to be used.
  47 + final bool isInitialized;
  48 +
  49 + /// Whether the mobile scanner is currently running.
  50 + ///
  51 + /// This is `true` if the camera is active.
  52 + final bool isRunning;
  53 +
  54 + /// The size of the camera output.
  55 + final Size size;
  56 +
  57 + /// The current state of the flashlight of the camera.
  58 + final TorchState torchState;
  59 +
  60 + /// The current zoom scale of the camera.
  61 + final double zoomScale;
  62 +
  63 + /// Create a copy of this state with the given parameters.
  64 + MobileScannerState copyWith({
  65 + int? availableCameras,
  66 + CameraFacing? cameraDirection,
  67 + MobileScannerException? error,
  68 + bool? isInitialized,
  69 + bool? isRunning,
  70 + Size? size,
  71 + TorchState? torchState,
  72 + double? zoomScale,
  73 + }) {
  74 + return MobileScannerState(
  75 + availableCameras: availableCameras ?? this.availableCameras,
  76 + cameraDirection: cameraDirection ?? this.cameraDirection,
  77 + error: error,
  78 + isInitialized: isInitialized ?? this.isInitialized,
  79 + isRunning: isRunning ?? this.isRunning,
  80 + size: size ?? this.size,
  81 + torchState: torchState ?? this.torchState,
  82 + zoomScale: zoomScale ?? this.zoomScale,
  83 + );
  84 + }
  85 +}
  1 +import 'dart:ui';
  2 +
  3 +import 'package:mobile_scanner/src/enums/barcode_format.dart';
  4 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
  5 +import 'package:mobile_scanner/src/enums/detection_speed.dart';
  6 +
  7 +/// This class defines the different start options for the mobile scanner.
  8 +class StartOptions {
  9 + const StartOptions({
  10 + required this.cameraDirection,
  11 + required this.cameraResolution,
  12 + required this.detectionSpeed,
  13 + required this.detectionTimeoutMs,
  14 + required this.formats,
  15 + required this.returnImage,
  16 + required this.torchEnabled,
  17 + });
  18 +
  19 + /// The direction for the camera.
  20 + final CameraFacing cameraDirection;
  21 +
  22 + /// The desired camera resolution for the scanner.
  23 + final Size? cameraResolution;
  24 +
  25 + /// The detection speed for the scanner.
  26 + final DetectionSpeed detectionSpeed;
  27 +
  28 + /// The detection timeout for the scanner, in milliseconds.
  29 + final int detectionTimeoutMs;
  30 +
  31 + /// The barcode formats to detect.
  32 + final List<BarcodeFormat> formats;
  33 +
  34 + /// Whether the detected barcodes should provide their image data.
  35 + final bool returnImage;
  36 +
  37 + /// Whether the torch should be turned on when the scanner starts.
  38 + final bool torchEnabled;
  39 +
  40 + Map<String, Object?> toMap() {
  41 + return <String, Object?>{
  42 + if (cameraResolution != null)
  43 + 'cameraResolution': <int>[
  44 + cameraResolution!.width.toInt(),
  45 + cameraResolution!.height.toInt(),
  46 + ],
  47 + 'facing': cameraDirection.rawValue,
  48 + if (formats.isNotEmpty)
  49 + 'formats': formats.map((f) => f.rawValue).toList(),
  50 + 'returnImage': returnImage,
  51 + 'speed': detectionSpeed.rawValue,
  52 + 'timeout': detectionTimeoutMs,
  53 + 'torch': torchEnabled,
  54 + };
  55 + }
  56 +}