Julian Steenbakker
Committed by GitHub

Merge branch 'master' into dependabot/gradle/android/com.android.tools.build-gradle-7.3.1

@@ -11,7 +11,7 @@ jobs: @@ -11,7 +11,7 @@ jobs:
11 analysis: 11 analysis:
12 runs-on: ubuntu-latest 12 runs-on: ubuntu-latest
13 steps: 13 steps:
14 - - uses: actions/checkout@v3.0.2 14 + - uses: actions/checkout@v3.1.0
15 - uses: actions/setup-java@v3.5.1 15 - uses: actions/setup-java@v3.5.1
16 with: 16 with:
17 java-version: 11 17 java-version: 11
@@ -28,7 +28,7 @@ jobs: @@ -28,7 +28,7 @@ jobs:
28 formatting: 28 formatting:
29 runs-on: ubuntu-latest 29 runs-on: ubuntu-latest
30 steps: 30 steps:
31 - - uses: actions/checkout@v3.0.2 31 + - uses: actions/checkout@v3.1.0
32 - uses: actions/setup-java@v3.5.1 32 - uses: actions/setup-java@v3.5.1
33 with: 33 with:
34 java-version: 11 34 java-version: 11
  1 +## NEXT
  2 +Breaking changes:
  3 +* [iOS] The minimum deployment target is now 11.0 or higher.
  4 +* [iOS] Updated POD dependencies
  5 +
1 ## 3.0.0-beta.1 6 ## 3.0.0-beta.1
2 Breaking changes: 7 Breaking changes:
3 * [Android] SDK updated to SDK 33. 8 * [Android] SDK updated to SDK 33.
@@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner' @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner'
2 version '1.0-SNAPSHOT' 2 version '1.0-SNAPSHOT'
3 3
4 buildscript { 4 buildscript {
5 - ext.kotlin_version = '1.7.10' 5 + ext.kotlin_version = '1.7.20'
6 repositories { 6 repositories {
7 google() 7 google()
8 mavenCentral() 8 mavenCentral()
@@ -50,7 +50,7 @@ dependencies { @@ -50,7 +50,7 @@ dependencies {
50 // Use this dependency to bundle the model with your app 50 // Use this dependency to bundle the model with your app
51 implementation 'com.google.mlkit:barcode-scanning:17.0.2' 51 implementation 'com.google.mlkit:barcode-scanning:17.0.2'
52 // Use this dependency to use the dynamically downloaded model in Google Play Services 52 // Use this dependency to use the dynamically downloaded model in Google Play Services
53 -// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0' 53 +// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
54 54
55 implementation 'androidx.camera:camera-camera2:1.1.0' 55 implementation 'androidx.camera:camera-camera2:1.1.0'
56 implementation 'androidx.camera:camera-lifecycle:1.1.0' 56 implementation 'androidx.camera:camera-lifecycle:1.1.0'
@@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner @@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner
3 import android.Manifest 3 import android.Manifest
4 import android.app.Activity 4 import android.app.Activity
5 import android.content.pm.PackageManager 5 import android.content.pm.PackageManager
  6 +import android.graphics.ImageFormat
6 import android.graphics.Point 7 import android.graphics.Point
  8 +import android.graphics.Rect
  9 +import android.graphics.YuvImage
  10 +import android.media.Image
7 import android.net.Uri 11 import android.net.Uri
8 import android.util.Log 12 import android.util.Log
9 import android.util.Size 13 import android.util.Size
@@ -23,11 +27,13 @@ import io.flutter.plugin.common.MethodCall @@ -23,11 +27,13 @@ import io.flutter.plugin.common.MethodCall
23 import io.flutter.plugin.common.MethodChannel 27 import io.flutter.plugin.common.MethodChannel
24 import io.flutter.plugin.common.PluginRegistry 28 import io.flutter.plugin.common.PluginRegistry
25 import io.flutter.view.TextureRegistry 29 import io.flutter.view.TextureRegistry
  30 +import java.io.ByteArrayOutputStream
26 import java.io.File 31 import java.io.File
27 32
28 33
29 -class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)  
30 - : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { 34 +class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) :
  35 + MethodChannel.MethodCallHandler, EventChannel.StreamHandler,
  36 + PluginRegistry.RequestPermissionsResultListener {
31 companion object { 37 companion object {
32 /** 38 /**
33 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. 39 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
@@ -70,15 +76,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -70,15 +76,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
70 sink = null 76 sink = null
71 } 77 }
72 78
73 - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean { 79 + override fun onRequestPermissionsResult(
  80 + requestCode: Int,
  81 + permissions: Array<out String>,
  82 + grantResults: IntArray
  83 + ): Boolean {
74 return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false 84 return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
75 } 85 }
76 86
77 private fun checkPermission(result: MethodChannel.Result) { 87 private fun checkPermission(result: MethodChannel.Result) {
78 // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized 88 // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
79 val state = 89 val state =
80 - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1  
81 - else 0 90 + if (ContextCompat.checkSelfPermission(
  91 + activity,
  92 + Manifest.permission.CAMERA
  93 + ) == PackageManager.PERMISSION_GRANTED
  94 + ) 1
  95 + else 0
82 result.success(state) 96 result.success(state)
83 } 97 }
84 98
@@ -96,32 +110,83 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -96,32 +110,83 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
96 val permissions = arrayOf(Manifest.permission.CAMERA) 110 val permissions = arrayOf(Manifest.permission.CAMERA)
97 ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) 111 ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
98 } 112 }
99 - 113 +// var lastScanned: List<Barcode>? = null
  114 +// var isAnalyzing: Boolean = false
100 115
101 @ExperimentalGetImage 116 @ExperimentalGetImage
102 val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format 117 val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
103 -// when (analyzeMode) {  
104 -// AnalyzeMode.BARCODE -> {  
105 - val mediaImage = imageProxy.image ?: return@Analyzer  
106 - val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)  
107 -  
108 - scanner.process(inputImage)  
109 - .addOnSuccessListener { barcodes ->  
110 - for (barcode in barcodes) {  
111 - val event = mapOf("name" to "barcode", "data" to barcode.data)  
112 - sink?.success(event)  
113 - }  
114 - }  
115 - .addOnFailureListener { e -> Log.e(TAG, e.message, e) }  
116 - .addOnCompleteListener { imageProxy.close() }  
117 -// }  
118 -// else -> imageProxy.close()  
119 -// } 118 +
  119 + val mediaImage = imageProxy.image ?: return@Analyzer
  120 + val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
  121 +
  122 + scanner.process(inputImage)
  123 + .addOnSuccessListener { barcodes ->
  124 +// if (isAnalyzing) {
  125 +// Log.d("scanner", "SKIPPING" )
  126 +// return@addOnSuccessListener
  127 +// }
  128 +// isAnalyzing = true
  129 + val barcodeMap = barcodes.map { barcode -> barcode.data }
  130 + if (barcodeMap.isNotEmpty()) {
  131 + sink?.success(mapOf(
  132 + "name" to "barcode",
  133 + "data" to barcodeMap,
  134 + "image" to mediaImage.toByteArray()
  135 + ))
  136 + }
  137 +// for (barcode in barcodes) {
  138 +//// if (lastScanned?.contains(barcodes.first) == true) continue;
  139 +// if (lastScanned == null) {
  140 +// lastScanned = barcodes
  141 +// } else if (lastScanned!!.contains(barcode)) {
  142 +// // Duplicate, don't send image
  143 +// sink?.success(mapOf(
  144 +// "name" to "barcode",
  145 +// "data" to barcode.data,
  146 +// ))
  147 +// } else {
  148 +// if (byteArray.isEmpty()) {
  149 +// Log.d("scanner", "EMPTY" )
  150 +// return@addOnSuccessListener
  151 +// }
  152 +//
  153 +// Log.d("scanner", "SCANNED IMAGE: $byteArray")
  154 +// lastScanned = barcodes;
  155 +//
  156 +//
  157 +// }
  158 +//
  159 +// }
  160 +// isAnalyzing = false
  161 + }
  162 + .addOnFailureListener { e -> sink?.success(mapOf(
  163 + "name" to "error",
  164 + "data" to e.localizedMessage
  165 + )) }
  166 + .addOnCompleteListener { imageProxy.close() }
120 } 167 }
121 168
  169 + private fun Image.toByteArray(): ByteArray {
  170 + val yBuffer = planes[0].buffer // Y
  171 + val vuBuffer = planes[2].buffer // VU
  172 +
  173 + val ySize = yBuffer.remaining()
  174 + val vuSize = vuBuffer.remaining()
  175 +
  176 + val nv21 = ByteArray(ySize + vuSize)
  177 +
  178 + yBuffer.get(nv21, 0, ySize)
  179 + vuBuffer.get(nv21, ySize, vuSize)
  180 +
  181 + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
  182 + val out = ByteArrayOutputStream()
  183 + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
  184 + return out.toByteArray()
  185 + }
122 186
123 private var scanner = BarcodeScanning.getClient() 187 private var scanner = BarcodeScanning.getClient()
124 188
  189 +
125 @ExperimentalGetImage 190 @ExperimentalGetImage
126 private fun start(call: MethodCall, result: MethodChannel.Result) { 191 private fun start(call: MethodCall, result: MethodChannel.Result) {
127 if (camera?.cameraInfo != null && preview != null && textureEntry != null) { 192 if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
@@ -129,14 +194,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -129,14 +194,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
129 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 194 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
130 val width = resolution.width.toDouble() 195 val width = resolution.width.toDouble()
131 val height = resolution.height.toDouble() 196 val height = resolution.height.toDouble()
132 - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)  
133 - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) 197 + val size = if (portrait) mapOf(
  198 + "width" to width,
  199 + "height" to height
  200 + ) else mapOf("width" to height, "height" to width)
  201 + val answer = mapOf(
  202 + "textureId" to textureEntry!!.id(),
  203 + "size" to size,
  204 + "torchable" to camera!!.cameraInfo.hasFlashUnit()
  205 + )
134 result.success(answer) 206 result.success(answer)
135 } else { 207 } else {
136 val facing: Int = call.argument<Int>("facing") ?: 0 208 val facing: Int = call.argument<Int>("facing") ?: 0
137 val ratio: Int? = call.argument<Int>("ratio") 209 val ratio: Int? = call.argument<Int>("ratio")
138 val torch: Boolean = call.argument<Boolean>("torch") ?: false 210 val torch: Boolean = call.argument<Boolean>("torch") ?: false
139 val formats: List<Int>? = call.argument<List<Int>>("formats") 211 val formats: List<Int>? = call.argument<List<Int>>("formats")
  212 +// val analyzerWidth = call.argument<Int>("ratio")
  213 +// val analyzeRHEIG = call.argument<Int>("ratio")
140 214
141 if (formats != null) { 215 if (formats != null) {
142 val formatsList: MutableList<Int> = mutableListOf() 216 val formatsList: MutableList<Int> = mutableListOf()
@@ -144,9 +218,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -144,9 +218,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
144 formatsList.add(BarcodeFormats.values()[index].intValue) 218 formatsList.add(BarcodeFormats.values()[index].intValue)
145 } 219 }
146 scanner = if (formatsList.size == 1) { 220 scanner = if (formatsList.size == 1) {
147 - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build()) 221 + BarcodeScanning.getClient(
  222 + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
  223 + .build()
  224 + )
148 } else { 225 } else {
149 - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray()).build()) 226 + BarcodeScanning.getClient(
  227 + BarcodeScannerOptions.Builder().setBarcodeFormats(
  228 + formatsList.first(),
  229 + *formatsList.subList(1, formatsList.size).toIntArray()
  230 + ).build()
  231 + )
150 } 232 }
151 } 233 }
152 234
@@ -168,7 +250,10 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -168,7 +250,10 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
168 // Preview 250 // Preview
169 val surfaceProvider = Preview.SurfaceProvider { request -> 251 val surfaceProvider = Preview.SurfaceProvider { request ->
170 val texture = textureEntry!!.surfaceTexture() 252 val texture = textureEntry!!.surfaceTexture()
171 - texture.setDefaultBufferSize(request.resolution.width, request.resolution.height) 253 + texture.setDefaultBufferSize(
  254 + request.resolution.width,
  255 + request.resolution.height
  256 + )
172 val surface = Surface(texture) 257 val surface = Surface(texture)
173 request.provideSurface(surface, executor) { } 258 request.provideSurface(surface, executor) { }
174 } 259 }
@@ -182,16 +267,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -182,16 +267,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
182 267
183 // Build the analyzer to be passed on to MLKit 268 // Build the analyzer to be passed on to MLKit
184 val analysisBuilder = ImageAnalysis.Builder() 269 val analysisBuilder = ImageAnalysis.Builder()
185 - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) 270 + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
186 if (ratio != null) { 271 if (ratio != null) {
187 analysisBuilder.setTargetAspectRatio(ratio) 272 analysisBuilder.setTargetAspectRatio(ratio)
188 } 273 }
  274 +// analysisBuilder.setTargetResolution(Size(1440, 1920))
189 val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } 275 val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }
190 276
191 // Select the correct camera 277 // Select the correct camera
192 - val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA 278 + val selector =
  279 + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
193 280
194 - camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview, analysis) 281 + camera = cameraProvider!!.bindToLifecycle(
  282 + activity as LifecycleOwner,
  283 + selector,
  284 + preview,
  285 + analysis
  286 + )
195 287
196 val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) 288 val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
197 val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) 289 val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
@@ -216,8 +308,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -216,8 +308,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
216 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 308 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
217 val width = resolution.width.toDouble() 309 val width = resolution.width.toDouble()
218 val height = resolution.height.toDouble() 310 val height = resolution.height.toDouble()
219 - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)  
220 - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) 311 + val size = if (portrait) mapOf(
  312 + "width" to width,
  313 + "height" to height
  314 + ) else mapOf("width" to height, "height" to width)
  315 + val answer = mapOf(
  316 + "textureId" to textureEntry!!.id(),
  317 + "size" to size,
  318 + "torchable" to camera!!.cameraInfo.hasFlashUnit()
  319 + )
221 result.success(answer) 320 result.success(answer)
222 }, executor) 321 }, executor)
223 } 322 }
@@ -225,7 +324,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -225,7 +324,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
225 324
226 private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { 325 private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
227 if (camera == null) { 326 if (camera == null) {
228 - result.error(TAG,"Called toggleTorch() while stopped!", null) 327 + result.error(TAG, "Called toggleTorch() while stopped!", null)
229 return 328 return
230 } 329 }
231 camera!!.cameraControl.enableTorch(call.arguments == 1) 330 camera!!.cameraControl.enableTorch(call.arguments == 1)
@@ -238,7 +337,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -238,7 +337,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
238 // } 337 // }
239 338
240 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { 339 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
241 - val uri = Uri.fromFile( File(call.arguments.toString())) 340 + val uri = Uri.fromFile(File(call.arguments.toString()))
242 val inputImage = InputImage.fromFilePath(activity, uri) 341 val inputImage = InputImage.fromFilePath(activity, uri)
243 342
244 var barcodeFound = false 343 var barcodeFound = false
@@ -249,15 +348,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -249,15 +348,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
249 sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) 348 sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
250 } 349 }
251 } 350 }
252 - .addOnFailureListener { e -> Log.e(TAG, e.message, e)  
253 - result.error(TAG, e.message, e)} 351 + .addOnFailureListener { e ->
  352 + Log.e(TAG, e.message, e)
  353 + result.error(TAG, e.message, e)
  354 + }
254 .addOnCompleteListener { result.success(barcodeFound) } 355 .addOnCompleteListener { result.success(barcodeFound) }
255 356
256 } 357 }
257 358
258 private fun stop(result: MethodChannel.Result) { 359 private fun stop(result: MethodChannel.Result) {
259 if (camera == null && preview == null) { 360 if (camera == null && preview == null) {
260 - result.error(TAG,"Called stop() while already stopped!", null) 361 + result.error(TAG, "Called stop() while already stopped!", null)
261 return 362 return
262 } 363 }
263 364
@@ -277,41 +378,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -277,41 +378,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
277 378
278 379
279 private val Barcode.data: Map<String, Any?> 380 private val Barcode.data: Map<String, Any?>
280 - get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,  
281 - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,  
282 - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,  
283 - "driverLicense" to driverLicense?.data, "email" to email?.data,  
284 - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,  
285 - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue) 381 + get() = mapOf(
  382 + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
  383 + "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
  384 + "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
  385 + "driverLicense" to driverLicense?.data, "email" to email?.data,
  386 + "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
  387 + "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
  388 + )
286 389
287 private val Point.data: Map<String, Double> 390 private val Point.data: Map<String, Double>
288 get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) 391 get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
289 392
290 private val Barcode.CalendarEvent.data: Map<String, Any?> 393 private val Barcode.CalendarEvent.data: Map<String, Any?>
291 - get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location,  
292 - "organizer" to organizer, "start" to start?.rawValue, "status" to status,  
293 - "summary" to summary) 394 + get() = mapOf(
  395 + "description" to description, "end" to end?.rawValue, "location" to location,
  396 + "organizer" to organizer, "start" to start?.rawValue, "status" to status,
  397 + "summary" to summary
  398 + )
294 399
295 private val Barcode.ContactInfo.data: Map<String, Any?> 400 private val Barcode.ContactInfo.data: Map<String, Any?>
296 - get() = mapOf("addresses" to addresses.map { address -> address.data },  
297 - "emails" to emails.map { email -> email.data }, "name" to name?.data,  
298 - "organization" to organization, "phones" to phones.map { phone -> phone.data },  
299 - "title" to title, "urls" to urls) 401 + get() = mapOf(
  402 + "addresses" to addresses.map { address -> address.data },
  403 + "emails" to emails.map { email -> email.data }, "name" to name?.data,
  404 + "organization" to organization, "phones" to phones.map { phone -> phone.data },
  405 + "title" to title, "urls" to urls
  406 + )
300 407
301 private val Barcode.Address.data: Map<String, Any?> 408 private val Barcode.Address.data: Map<String, Any?>
302 - get() = mapOf("addressLines" to addressLines.map { addressLine -> addressLine.toString() }, "type" to type) 409 + get() = mapOf(
  410 + "addressLines" to addressLines.map { addressLine -> addressLine.toString() },
  411 + "type" to type
  412 + )
303 413
304 private val Barcode.PersonName.data: Map<String, Any?> 414 private val Barcode.PersonName.data: Map<String, Any?>
305 - get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last,  
306 - "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,  
307 - "suffix" to suffix) 415 + get() = mapOf(
  416 + "first" to first, "formattedName" to formattedName, "last" to last,
  417 + "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
  418 + "suffix" to suffix
  419 + )
308 420
309 private val Barcode.DriverLicense.data: Map<String, Any?> 421 private val Barcode.DriverLicense.data: Map<String, Any?>
310 - get() = mapOf("addressCity" to addressCity, "addressState" to addressState,  
311 - "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,  
312 - "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,  
313 - "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,  
314 - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName) 422 + get() = mapOf(
  423 + "addressCity" to addressCity, "addressState" to addressState,
  424 + "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
  425 + "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
  426 + "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
  427 + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
  428 + )
315 429
316 private val Barcode.Email.data: Map<String, Any?> 430 private val Barcode.Email.data: Map<String, Any?>
317 get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) 431 get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
@@ -330,4 +444,4 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -330,4 +444,4 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
330 444
331 private val Barcode.WiFi.data: Map<String, Any?> 445 private val Barcode.WiFi.data: Map<String, Any?>
332 get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) 446 get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
333 -} 447 +}
@@ -21,6 +21,6 @@ @@ -21,6 +21,6 @@
21 <key>CFBundleVersion</key> 21 <key>CFBundleVersion</key>
22 <string>1.0</string> 22 <string>1.0</string>
23 <key>MinimumOSVersion</key> 23 <key>MinimumOSVersion</key>
24 - <string>9.0</string> 24 + <string>11.0</string>
25 </dict> 25 </dict>
26 </plist> 26 </plist>
1 # Uncomment this line to define a global platform for your project 1 # Uncomment this line to define a global platform for your project
2 -platform :ios, '10.0' 2 +platform :ios, '11.0'
3 3
4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -37,5 +37,9 @@ end @@ -37,5 +37,9 @@ end
37 post_install do |installer| 37 post_install do |installer|
38 installer.pods_project.targets.each do |target| 38 installer.pods_project.targets.each do |target|
39 flutter_additional_ios_build_settings(target) 39 flutter_additional_ios_build_settings(target)
  40 + target.build_configurations.each do |config|
  41 + config.build_settings['ENABLE_BITCODE'] = 'NO'
  42 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
  43 + end
40 end 44 end
41 end 45 end
@@ -339,7 +339,7 @@ @@ -339,7 +339,7 @@
339 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 339 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
340 GCC_WARN_UNUSED_FUNCTION = YES; 340 GCC_WARN_UNUSED_FUNCTION = YES;
341 GCC_WARN_UNUSED_VARIABLE = YES; 341 GCC_WARN_UNUSED_VARIABLE = YES;
342 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 342 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
343 MTL_ENABLE_DEBUG_INFO = NO; 343 MTL_ENABLE_DEBUG_INFO = NO;
344 SDKROOT = iphoneos; 344 SDKROOT = iphoneos;
345 SUPPORTED_PLATFORMS = iphoneos; 345 SUPPORTED_PLATFORMS = iphoneos;
@@ -355,7 +355,7 @@ @@ -355,7 +355,7 @@
355 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
356 CLANG_ENABLE_MODULES = YES; 356 CLANG_ENABLE_MODULES = YES;
357 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 357 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
358 - DEVELOPMENT_TEAM = RCH2VG82SH; 358 + DEVELOPMENT_TEAM = 75Y2P2WSQQ;
359 ENABLE_BITCODE = NO; 359 ENABLE_BITCODE = NO;
360 INFOPLIST_FILE = Runner/Info.plist; 360 INFOPLIST_FILE = Runner/Info.plist;
361 LD_RUNPATH_SEARCH_PATHS = ( 361 LD_RUNPATH_SEARCH_PATHS = (
@@ -417,7 +417,7 @@ @@ -417,7 +417,7 @@
417 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 417 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
418 GCC_WARN_UNUSED_FUNCTION = YES; 418 GCC_WARN_UNUSED_FUNCTION = YES;
419 GCC_WARN_UNUSED_VARIABLE = YES; 419 GCC_WARN_UNUSED_VARIABLE = YES;
420 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 420 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
421 MTL_ENABLE_DEBUG_INFO = YES; 421 MTL_ENABLE_DEBUG_INFO = YES;
422 ONLY_ACTIVE_ARCH = YES; 422 ONLY_ACTIVE_ARCH = YES;
423 SDKROOT = iphoneos; 423 SDKROOT = iphoneos;
@@ -466,7 +466,7 @@ @@ -466,7 +466,7 @@
466 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 466 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
467 GCC_WARN_UNUSED_FUNCTION = YES; 467 GCC_WARN_UNUSED_FUNCTION = YES;
468 GCC_WARN_UNUSED_VARIABLE = YES; 468 GCC_WARN_UNUSED_VARIABLE = YES;
469 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 469 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
470 MTL_ENABLE_DEBUG_INFO = NO; 470 MTL_ENABLE_DEBUG_INFO = NO;
471 SDKROOT = iphoneos; 471 SDKROOT = iphoneos;
472 SUPPORTED_PLATFORMS = iphoneos; 472 SUPPORTED_PLATFORMS = iphoneos;
@@ -484,7 +484,7 @@ @@ -484,7 +484,7 @@
484 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 484 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
485 CLANG_ENABLE_MODULES = YES; 485 CLANG_ENABLE_MODULES = YES;
486 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 486 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
487 - DEVELOPMENT_TEAM = RCH2VG82SH; 487 + DEVELOPMENT_TEAM = 75Y2P2WSQQ;
488 ENABLE_BITCODE = NO; 488 ENABLE_BITCODE = NO;
489 INFOPLIST_FILE = Runner/Info.plist; 489 INFOPLIST_FILE = Runner/Info.plist;
490 LD_RUNPATH_SEARCH_PATHS = ( 490 LD_RUNPATH_SEARCH_PATHS = (
@@ -507,7 +507,7 @@ @@ -507,7 +507,7 @@
507 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 507 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
508 CLANG_ENABLE_MODULES = YES; 508 CLANG_ENABLE_MODULES = YES;
509 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 509 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
510 - DEVELOPMENT_TEAM = RCH2VG82SH; 510 + DEVELOPMENT_TEAM = 75Y2P2WSQQ;
511 ENABLE_BITCODE = NO; 511 ENABLE_BITCODE = NO;
512 INFOPLIST_FILE = Runner/Info.plist; 512 INFOPLIST_FILE = Runner/Info.plist;
513 LD_RUNPATH_SEARCH_PATHS = ( 513 LD_RUNPATH_SEARCH_PATHS = (
  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 BarcodeListScannerWithController extends StatefulWidget {
  6 + const BarcodeListScannerWithController({Key? key}) : super(key: key);
  7 +
  8 + @override
  9 + _BarcodeListScannerWithControllerState createState() =>
  10 + _BarcodeListScannerWithControllerState();
  11 +}
  12 +
  13 +class _BarcodeListScannerWithControllerState
  14 + extends State<BarcodeListScannerWithController>
  15 + with SingleTickerProviderStateMixin {
  16 + BarcodeCapture? barcodeCapture;
  17 +
  18 + MobileScannerController controller = MobileScannerController(
  19 + torchEnabled: true,
  20 + // formats: [BarcodeFormat.qrCode]
  21 + // facing: CameraFacing.front,
  22 + );
  23 +
  24 + bool isStarted = true;
  25 +
  26 + @override
  27 + Widget build(BuildContext context) {
  28 + return Scaffold(
  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 + // allowDuplicates: true,
  38 + // controller: MobileScannerController(
  39 + // torchEnabled: true,
  40 + // facing: CameraFacing.front,
  41 + // ),
  42 + onDetect: (barcodeCapture, arguments) {
  43 + setState(() {
  44 + this.barcodeCapture = barcodeCapture;
  45 + });
  46 + },
  47 + ),
  48 + Align(
  49 + alignment: Alignment.bottomCenter,
  50 + child: Container(
  51 + alignment: Alignment.bottomCenter,
  52 + height: 100,
  53 + color: Colors.black.withOpacity(0.4),
  54 + child: Row(
  55 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  56 + children: [
  57 + IconButton(
  58 + color: Colors.white,
  59 + icon: ValueListenableBuilder(
  60 + valueListenable: controller.torchState,
  61 + builder: (context, state, child) {
  62 + if (state == null) {
  63 + return const Icon(
  64 + Icons.flash_off,
  65 + color: Colors.grey,
  66 + );
  67 + }
  68 + switch (state as TorchState) {
  69 + case TorchState.off:
  70 + return const Icon(
  71 + Icons.flash_off,
  72 + color: Colors.grey,
  73 + );
  74 + case TorchState.on:
  75 + return const Icon(
  76 + Icons.flash_on,
  77 + color: Colors.yellow,
  78 + );
  79 + }
  80 + },
  81 + ),
  82 + iconSize: 32.0,
  83 + onPressed: () => controller.toggleTorch(),
  84 + ),
  85 + IconButton(
  86 + color: Colors.white,
  87 + icon: isStarted
  88 + ? const Icon(Icons.stop)
  89 + : const Icon(Icons.play_arrow),
  90 + iconSize: 32.0,
  91 + onPressed: () => setState(() {
  92 + isStarted ? controller.stop() : controller.start();
  93 + isStarted = !isStarted;
  94 + }),
  95 + ),
  96 + Center(
  97 + child: SizedBox(
  98 + width: MediaQuery.of(context).size.width - 200,
  99 + height: 50,
  100 + child: FittedBox(
  101 + child: Text(
  102 + '${barcodeCapture?.barcodes.map((e) => e.rawValue)}',
  103 + overflow: TextOverflow.fade,
  104 + style: Theme.of(context)
  105 + .textTheme
  106 + .headline4!
  107 + .copyWith(color: Colors.white),
  108 + ),
  109 + ),
  110 + ),
  111 + ),
  112 + IconButton(
  113 + color: Colors.white,
  114 + icon: ValueListenableBuilder(
  115 + valueListenable: controller.cameraFacingState,
  116 + builder: (context, state, child) {
  117 + if (state == null) {
  118 + return const Icon(Icons.camera_front);
  119 + }
  120 + switch (state as CameraFacing) {
  121 + case CameraFacing.front:
  122 + return const Icon(Icons.camera_front);
  123 + case CameraFacing.back:
  124 + return const Icon(Icons.camera_rear);
  125 + }
  126 + },
  127 + ),
  128 + iconSize: 32.0,
  129 + onPressed: () => controller.switchCamera(),
  130 + ),
  131 + IconButton(
  132 + color: Colors.white,
  133 + icon: const Icon(Icons.image),
  134 + iconSize: 32.0,
  135 + onPressed: () async {
  136 + final ImagePicker picker = ImagePicker();
  137 + // Pick an image
  138 + final XFile? image = await picker.pickImage(
  139 + source: ImageSource.gallery,
  140 + );
  141 + if (image != null) {
  142 + if (await controller.analyzeImage(image.path)) {
  143 + if (!mounted) return;
  144 + ScaffoldMessenger.of(context).showSnackBar(
  145 + const SnackBar(
  146 + content: Text('Barcode found!'),
  147 + backgroundColor: Colors.green,
  148 + ),
  149 + );
  150 + } else {
  151 + if (!mounted) return;
  152 + ScaffoldMessenger.of(context).showSnackBar(
  153 + const SnackBar(
  154 + content: Text('No barcode found!'),
  155 + backgroundColor: Colors.red,
  156 + ),
  157 + );
  158 + }
  159 + }
  160 + },
  161 + ),
  162 + ],
  163 + ),
  164 + ),
  165 + ),
  166 + ],
  167 + );
  168 + },
  169 + ),
  170 + );
  171 + }
  172 +}
@@ -13,10 +13,10 @@ class BarcodeScannerWithController extends StatefulWidget { @@ -13,10 +13,10 @@ class BarcodeScannerWithController extends StatefulWidget {
13 class _BarcodeScannerWithControllerState 13 class _BarcodeScannerWithControllerState
14 extends State<BarcodeScannerWithController> 14 extends State<BarcodeScannerWithController>
15 with SingleTickerProviderStateMixin { 15 with SingleTickerProviderStateMixin {
16 - String? barcode; 16 + BarcodeCapture? barcode;
17 17
18 MobileScannerController controller = MobileScannerController( 18 MobileScannerController controller = MobileScannerController(
19 - torchEnabled: true, 19 + torchEnabled: true, detectionSpeed: DetectionSpeed.unrestricted,
20 // formats: [BarcodeFormat.qrCode] 20 // formats: [BarcodeFormat.qrCode]
21 // facing: CameraFacing.front, 21 // facing: CameraFacing.front,
22 ); 22 );
@@ -41,7 +41,7 @@ class _BarcodeScannerWithControllerState @@ -41,7 +41,7 @@ class _BarcodeScannerWithControllerState
41 // ), 41 // ),
42 onDetect: (barcode, args) { 42 onDetect: (barcode, args) {
43 setState(() { 43 setState(() {
44 - this.barcode = barcode.rawValue; 44 + this.barcode = barcode;
45 }); 45 });
46 }, 46 },
47 ), 47 ),
@@ -99,7 +99,8 @@ class _BarcodeScannerWithControllerState @@ -99,7 +99,8 @@ class _BarcodeScannerWithControllerState
99 height: 50, 99 height: 50,
100 child: FittedBox( 100 child: FittedBox(
101 child: Text( 101 child: Text(
102 - barcode ?? 'Scan something!', 102 + barcode?.barcodes.first.rawValue ??
  103 + 'Scan something!',
103 overflow: TextOverflow.fade, 104 overflow: TextOverflow.fade,
104 style: Theme.of(context) 105 style: Theme.of(context)
105 .textTheme 106 .textTheme
1 -import 'dart:typed_data'; 1 +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';
@@ -14,11 +14,11 @@ class BarcodeScannerReturningImage extends StatefulWidget { @@ -14,11 +14,11 @@ class BarcodeScannerReturningImage extends StatefulWidget {
14 class _BarcodeScannerReturningImageState 14 class _BarcodeScannerReturningImageState
15 extends State<BarcodeScannerReturningImage> 15 extends State<BarcodeScannerReturningImage>
16 with SingleTickerProviderStateMixin { 16 with SingleTickerProviderStateMixin {
17 - String? barcode;  
18 - Uint8List? image; 17 + BarcodeCapture? barcode;
  18 + MobileScannerArguments? arguments;
19 19
20 MobileScannerController controller = MobileScannerController( 20 MobileScannerController controller = MobileScannerController(
21 - torchEnabled: true, 21 + // torchEnabled: true,
22 returnImage: true, 22 returnImage: true,
23 // formats: [BarcodeFormat.qrCode] 23 // formats: [BarcodeFormat.qrCode]
24 // facing: CameraFacing.front, 24 // facing: CameraFacing.front,
@@ -32,125 +32,146 @@ class _BarcodeScannerReturningImageState @@ -32,125 +32,146 @@ class _BarcodeScannerReturningImageState
32 backgroundColor: Colors.black, 32 backgroundColor: Colors.black,
33 body: Builder( 33 body: Builder(
34 builder: (context) { 34 builder: (context) {
35 - return Stack( 35 + return Column(
36 children: [ 36 children: [
37 - MobileScanner(  
38 - controller: controller,  
39 - fit: BoxFit.contain,  
40 - // allowDuplicates: true,  
41 - // controller: MobileScannerController(  
42 - // torchEnabled: true,  
43 - // facing: CameraFacing.front,  
44 - // ),  
45 - onDetect: (barcode, args) {  
46 - setState(() {  
47 - this.barcode = barcode.rawValue;  
48 - showDialog(  
49 - context: context,  
50 - builder: (context) => Image(  
51 - image: MemoryImage(image!),  
52 - fit: BoxFit.contain,  
53 - ),  
54 - );  
55 - image = barcode.image;  
56 - });  
57 - },  
58 - ),  
59 - Align(  
60 - alignment: Alignment.bottomCenter,  
61 - child: Container(  
62 - alignment: Alignment.bottomCenter,  
63 - height: 100,  
64 - color: Colors.black.withOpacity(0.4),  
65 - child: Row(  
66 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,  
67 - children: [  
68 - IconButton(  
69 - color: Colors.white,  
70 - icon: ValueListenableBuilder(  
71 - valueListenable: controller.torchState,  
72 - builder: (context, state, child) {  
73 - if (state == null) {  
74 - return const Icon(  
75 - Icons.flash_off,  
76 - color: Colors.grey,  
77 - );  
78 - }  
79 - switch (state as TorchState) {  
80 - case TorchState.off:  
81 - return const Icon(  
82 - Icons.flash_off,  
83 - color: Colors.grey,  
84 - );  
85 - case TorchState.on:  
86 - return const Icon(  
87 - Icons.flash_on,  
88 - color: Colors.yellow,  
89 - );  
90 - }  
91 - }, 37 + Container(
  38 + color: Colors.blueGrey,
  39 + width: double.infinity,
  40 + height: 0.33 * MediaQuery.of(context).size.height,
  41 + child: barcode?.image != null
  42 + ? Transform.rotate(
  43 + angle: 90 * pi / 180,
  44 + child: Image(
  45 + gaplessPlayback: true,
  46 + image: MemoryImage(barcode!.image!),
  47 + fit: BoxFit.contain,
92 ), 48 ),
93 - iconSize: 32.0,  
94 - onPressed: () => controller.toggleTorch(),  
95 - ),  
96 - IconButton( 49 + )
  50 + : const ColoredBox(
97 color: Colors.white, 51 color: Colors.white,
98 - icon: isStarted  
99 - ? const Icon(Icons.stop)  
100 - : const Icon(Icons.play_arrow),  
101 - iconSize: 32.0,  
102 - onPressed: () => setState(() {  
103 - isStarted ? controller.stop() : controller.start();  
104 - isStarted = !isStarted;  
105 - }),  
106 - ),  
107 - Center(  
108 - child: SizedBox(  
109 - width: MediaQuery.of(context).size.width - 200,  
110 - height: 50,  
111 - child: FittedBox(  
112 - child: Text(  
113 - barcode ?? 'Scan something!',  
114 - overflow: TextOverflow.fade,  
115 - style: Theme.of(context)  
116 - .textTheme  
117 - .headline4!  
118 - .copyWith(color: Colors.white),  
119 - ), 52 + child: Center(
  53 + child: Text(
  54 + 'Your scanned barcode will appear here!',
120 ), 55 ),
121 ), 56 ),
122 ), 57 ),
123 - IconButton(  
124 - color: Colors.white,  
125 - icon: ValueListenableBuilder(  
126 - valueListenable: controller.cameraFacingState,  
127 - builder: (context, state, child) {  
128 - if (state == null) {  
129 - return const Icon(Icons.camera_front);  
130 - }  
131 - switch (state as CameraFacing) {  
132 - case CameraFacing.front:  
133 - return const Icon(Icons.camera_front);  
134 - case CameraFacing.back:  
135 - return const Icon(Icons.camera_rear);  
136 - }  
137 - }, 58 + ),
  59 + Container(
  60 + height: 0.66 * MediaQuery.of(context).size.height,
  61 + color: Colors.grey,
  62 + child: Stack(
  63 + children: [
  64 + MobileScanner(
  65 + controller: controller,
  66 + fit: BoxFit.contain,
  67 + // allowDuplicates: true,
  68 + // controller: MobileScannerController(
  69 + // torchEnabled: true,
  70 + // facing: CameraFacing.front,
  71 + // ),
  72 + onDetect: (barcode, arguments) {
  73 + setState(() {
  74 + this.arguments = arguments;
  75 + this.barcode = barcode;
  76 + });
  77 + },
  78 + ),
  79 + Align(
  80 + alignment: Alignment.bottomCenter,
  81 + child: Container(
  82 + alignment: Alignment.bottomCenter,
  83 + height: 100,
  84 + color: Colors.black.withOpacity(0.4),
  85 + child: Row(
  86 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  87 + children: [
  88 + ColoredBox(
  89 + color: arguments != null && !arguments!.hasTorch
  90 + ? Colors.red
  91 + : Colors.white,
  92 + child: IconButton(
  93 + // color: ,
  94 + icon: ValueListenableBuilder(
  95 + valueListenable: controller.torchState,
  96 + builder: (context, state, child) {
  97 + if (state == null) {
  98 + return const Icon(
  99 + Icons.flash_off,
  100 + color: Colors.grey,
  101 + );
  102 + }
  103 + switch (state as TorchState) {
  104 + case TorchState.off:
  105 + return const Icon(
  106 + Icons.flash_off,
  107 + color: Colors.grey,
  108 + );
  109 + case TorchState.on:
  110 + return const Icon(
  111 + Icons.flash_on,
  112 + color: Colors.yellow,
  113 + );
  114 + }
  115 + },
  116 + ),
  117 + iconSize: 32.0,
  118 + onPressed: () => controller.toggleTorch(),
  119 + ),
  120 + ),
  121 + IconButton(
  122 + color: Colors.white,
  123 + icon: isStarted
  124 + ? const Icon(Icons.stop)
  125 + : const Icon(Icons.play_arrow),
  126 + iconSize: 32.0,
  127 + onPressed: () => setState(() {
  128 + isStarted
  129 + ? controller.stop()
  130 + : controller.start();
  131 + isStarted = !isStarted;
  132 + }),
  133 + ),
  134 + Center(
  135 + child: SizedBox(
  136 + width: MediaQuery.of(context).size.width - 200,
  137 + height: 50,
  138 + child: FittedBox(
  139 + child: Text(
  140 + barcode?.barcodes.first.rawValue ??
  141 + 'Scan something!',
  142 + overflow: TextOverflow.fade,
  143 + style: Theme.of(context)
  144 + .textTheme
  145 + .headline4!
  146 + .copyWith(color: Colors.white),
  147 + ),
  148 + ),
  149 + ),
  150 + ),
  151 + IconButton(
  152 + color: Colors.white,
  153 + icon: ValueListenableBuilder(
  154 + valueListenable: controller.cameraFacingState,
  155 + builder: (context, state, child) {
  156 + if (state == null) {
  157 + return const Icon(Icons.camera_front);
  158 + }
  159 + switch (state as CameraFacing) {
  160 + case CameraFacing.front:
  161 + return const Icon(Icons.camera_front);
  162 + case CameraFacing.back:
  163 + return const Icon(Icons.camera_rear);
  164 + }
  165 + },
  166 + ),
  167 + iconSize: 32.0,
  168 + onPressed: () => controller.switchCamera(),
  169 + ),
  170 + ],
138 ), 171 ),
139 - iconSize: 32.0,  
140 - onPressed: () => controller.switchCamera(),  
141 - ),  
142 - SizedBox(  
143 - width: 50,  
144 - height: 50,  
145 - child: image != null  
146 - ? Image(  
147 - image: MemoryImage(image!),  
148 - fit: BoxFit.contain,  
149 - )  
150 - : Container(),  
151 ), 172 ),
152 - ],  
153 - ), 173 + ),
  174 + ],
154 ), 175 ),
155 ), 176 ),
156 ], 177 ],
@@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget { @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget {
12 class _BarcodeScannerWithoutControllerState 12 class _BarcodeScannerWithoutControllerState
13 extends State<BarcodeScannerWithoutController> 13 extends State<BarcodeScannerWithoutController>
14 with SingleTickerProviderStateMixin { 14 with SingleTickerProviderStateMixin {
15 - String? barcode; 15 + BarcodeCapture? capture;
16 16
17 @override 17 @override
18 Widget build(BuildContext context) { 18 Widget build(BuildContext context) {
@@ -25,9 +25,9 @@ class _BarcodeScannerWithoutControllerState @@ -25,9 +25,9 @@ class _BarcodeScannerWithoutControllerState
25 MobileScanner( 25 MobileScanner(
26 fit: BoxFit.contain, 26 fit: BoxFit.contain,
27 // allowDuplicates: false, 27 // allowDuplicates: false,
28 - onDetect: (barcode, args) { 28 + onDetect: (capture, arguments) {
29 setState(() { 29 setState(() {
30 - this.barcode = barcode.rawValue; 30 + this.capture = capture;
31 }); 31 });
32 }, 32 },
33 ), 33 ),
@@ -46,7 +46,8 @@ class _BarcodeScannerWithoutControllerState @@ -46,7 +46,8 @@ class _BarcodeScannerWithoutControllerState
46 height: 50, 46 height: 50,
47 child: FittedBox( 47 child: FittedBox(
48 child: Text( 48 child: Text(
49 - barcode ?? 'Scan something!', 49 + capture?.barcodes.first.rawValue ??
  50 + 'Scan something!',
50 overflow: TextOverflow.fade, 51 overflow: TextOverflow.fade,
51 style: Theme.of(context) 52 style: Theme.of(context)
52 .textTheme 53 .textTheme
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
  2 +import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
2 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
3 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
@@ -22,6 +23,17 @@ class MyHome extends StatelessWidget { @@ -22,6 +23,17 @@ class MyHome extends StatelessWidget {
22 onPressed: () { 23 onPressed: () {
23 Navigator.of(context).push( 24 Navigator.of(context).push(
24 MaterialPageRoute( 25 MaterialPageRoute(
  26 + builder: (context) =>
  27 + const BarcodeListScannerWithController(),
  28 + ),
  29 + );
  30 + },
  31 + child: const Text('MobileScanner with List Controller'),
  32 + ),
  33 + ElevatedButton(
  34 + onPressed: () {
  35 + Navigator.of(context).push(
  36 + MaterialPageRoute(
25 builder: (context) => const BarcodeScannerWithController(), 37 builder: (context) => const BarcodeScannerWithController(),
26 ), 38 ),
27 ); 39 );
  1 +//
  2 +// BarcodeHandler.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 24/08/2022.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +public class BarcodeHandler: NSObject, FlutterStreamHandler {
  11 +
  12 + var event: [String: Any?] = [:]
  13 +
  14 + private var eventSink: FlutterEventSink?
  15 + private let eventChannel: FlutterEventChannel
  16 +
  17 + init(registrar: FlutterPluginRegistrar) {
  18 + eventChannel = FlutterEventChannel(name:
  19 + "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
  20 + super.init()
  21 + eventChannel.setStreamHandler(self)
  22 + }
  23 +
  24 + func publishEvent(_ event: [String: Any?]) {
  25 + self.event = event
  26 + eventSink?(event)
  27 + }
  28 +
  29 + public func onListen(withArguments arguments: Any?,
  30 + eventSink: @escaping FlutterEventSink) -> FlutterError? {
  31 + self.eventSink = eventSink
  32 + return nil
  33 + }
  34 +
  35 + public func onCancel(withArguments arguments: Any?) -> FlutterError? {
  36 + eventSink = nil
  37 + return nil
  38 + }
  39 +}
  1 +//
  2 +// SwiftMobileScanner.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 15/02/2022.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +import AVFoundation
  11 +import MLKitVision
  12 +import MLKitBarcodeScanning
  13 +
  14 +typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
  15 +
  16 +public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
  17 + /// Capture session of the camera
  18 + var captureSession: AVCaptureSession!
  19 +
  20 + /// The selected camera
  21 + var device: AVCaptureDevice!
  22 +
  23 + /// Barcode scanner for results
  24 + var scanner = BarcodeScanner.barcodeScanner()
  25 +
  26 + /// Return image buffer with the Barcode event
  27 + var returnImage: Bool = false
  28 +
  29 + /// Default position of camera
  30 + var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
  31 +
  32 + /// When results are found, this callback will be called
  33 + let mobileScannerCallback: MobileScannerCallback
  34 +
  35 + /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
  36 + private let registry: FlutterTextureRegistry?
  37 +
  38 + /// Image to be sent to the texture
  39 + var latestBuffer: CVImageBuffer!
  40 +
  41 + /// Texture id of the camera preview for Flutter
  42 + private var textureId: Int64!
  43 +
  44 + var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
  45 +
  46 + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) {
  47 + self.registry = registry
  48 + self.mobileScannerCallback = mobileScannerCallback
  49 + super.init()
  50 + }
  51 +
  52 + /// Check permissions for video
  53 + func checkPermission() -> Int {
  54 + let status = AVCaptureDevice.authorizationStatus(for: .video)
  55 + switch status {
  56 + case .notDetermined:
  57 + return 0
  58 + case .authorized:
  59 + return 1
  60 + default:
  61 + return 2
  62 + }
  63 + }
  64 +
  65 + /// Request permissions for video
  66 + func requestPermission(_ result: @escaping FlutterResult) {
  67 + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
  68 + }
  69 +
  70 + /// Start scanning for barcodes
  71 + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
  72 + self.detectionSpeed = detectionSpeed
  73 + if (device != nil) {
  74 + throw MobileScannerError.alreadyStarted
  75 + }
  76 +
  77 + scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
  78 + captureSession = AVCaptureSession()
  79 + textureId = registry?.register(self)
  80 +
  81 + // Open the camera device
  82 + if #available(iOS 10.0, *) {
  83 + device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first
  84 + } else {
  85 + device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first
  86 + }
  87 +
  88 + if (device == nil) {
  89 + throw MobileScannerError.noCamera
  90 + }
  91 +
  92 + // Enable the torch if parameter is set and torch is available
  93 + if (device.hasTorch && device.isTorchAvailable) {
  94 + do {
  95 + try device.lockForConfiguration()
  96 + device.torchMode = torch
  97 + device.unlockForConfiguration()
  98 + } catch {
  99 + throw MobileScannerError.torchError(error)
  100 + }
  101 + }
  102 +
  103 + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
  104 + captureSession.beginConfiguration()
  105 +
  106 + // Add device input
  107 + do {
  108 + let input = try AVCaptureDeviceInput(device: device)
  109 + captureSession.addInput(input)
  110 + } catch {
  111 + throw MobileScannerError.cameraError(error)
  112 + }
  113 +
  114 + captureSession.sessionPreset = AVCaptureSession.Preset.photo;
  115 + // Add video output.
  116 + let videoOutput = AVCaptureVideoDataOutput()
  117 +
  118 + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
  119 + videoOutput.alwaysDiscardsLateVideoFrames = true
  120 +
  121 + videoPosition = cameraPosition
  122 + // calls captureOutput()
  123 + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
  124 +
  125 + captureSession.addOutput(videoOutput)
  126 + for connection in videoOutput.connections {
  127 + connection.videoOrientation = .portrait
  128 + if cameraPosition == .front && connection.isVideoMirroringSupported {
  129 + connection.isVideoMirrored = true
  130 + }
  131 + }
  132 + captureSession.commitConfiguration()
  133 + captureSession.startRunning()
  134 + let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
  135 +
  136 + return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
  137 + }
  138 +
  139 + struct MobileScannerStartParameters {
  140 + var width: Double = 0.0
  141 + var height: Double = 0.0
  142 + var hasTorch = false
  143 + var textureId: Int64 = 0
  144 + }
  145 +
  146 + /// Stop scanning for barcodes
  147 + func stop() throws {
  148 + if (device == nil) {
  149 + throw MobileScannerError.alreadyStopped
  150 + }
  151 + captureSession.stopRunning()
  152 + for input in captureSession.inputs {
  153 + captureSession.removeInput(input)
  154 + }
  155 + for output in captureSession.outputs {
  156 + captureSession.removeOutput(output)
  157 + }
  158 + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
  159 + registry?.unregisterTexture(textureId)
  160 + textureId = nil
  161 + captureSession = nil
  162 + device = nil
  163 + }
  164 +
  165 + /// Toggle the flashlight between on and off
  166 + func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws {
  167 + if (device == nil) {
  168 + throw MobileScannerError.torchWhenStopped
  169 + }
  170 + do {
  171 + try device.lockForConfiguration()
  172 + device.torchMode = torch
  173 + device.unlockForConfiguration()
  174 + } catch {
  175 + throw MobileScannerError.torchError(error)
  176 + }
  177 + }
  178 +
  179 + /// Analyze a single image
  180 + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
  181 + let image = VisionImage(image: image)
  182 + image.orientation = imageOrientation(
  183 + deviceOrientation: UIDevice.current.orientation,
  184 + defaultOrientation: .portrait,
  185 + position: position
  186 + )
  187 +
  188 + scanner.process(image, completion: callback)
  189 + }
  190 +
  191 + var i = 0
  192 +
  193 + var barcodesString: Array<String?>?
  194 +
  195 + /// Gets called when a new image is added to the buffer
  196 + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  197 + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
  198 + print("Failed to get image buffer from sample buffer.")
  199 + return
  200 + }
  201 + latestBuffer = imageBuffer
  202 + registry?.textureFrameAvailable(textureId)
  203 + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
  204 + i = 0
  205 + let ciImage = latestBuffer.image
  206 +
  207 + let image = VisionImage(image: ciImage)
  208 + image.orientation = imageOrientation(
  209 + deviceOrientation: UIDevice.current.orientation,
  210 + defaultOrientation: .portrait,
  211 + position: videoPosition
  212 + )
  213 +
  214 + scanner.process(image) { [self] barcodes, error in
  215 + if (detectionSpeed == DetectionSpeed.noDuplicates) {
  216 + let newScannedBarcodes = barcodes?.map { barcode in
  217 + return barcode.rawValue
  218 + }
  219 + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
  220 + return
  221 + } else {
  222 + barcodesString = newScannedBarcodes
  223 + }
  224 + }
  225 +
  226 + mobileScannerCallback(barcodes, error, ciImage)
  227 + }
  228 + } else {
  229 + i+=1
  230 + }
  231 + }
  232 +
  233 + /// Convert image buffer to jpeg
  234 + private func ciImageToJpeg(ciImage: CIImage) -> Data {
  235 +
  236 + // let ciImage = CIImage(cvPixelBuffer: latestBuffer)
  237 + let context:CIContext = CIContext.init(options: nil)
  238 + let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
  239 + let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
  240 +
  241 + return uiImage.jpegData(compressionQuality: 0.8)!;
  242 + }
  243 +
  244 + /// Rotates images accordingly
  245 + func imageOrientation(
  246 + deviceOrientation: UIDeviceOrientation,
  247 + defaultOrientation: UIDeviceOrientation,
  248 + position: AVCaptureDevice.Position
  249 + ) -> UIImage.Orientation {
  250 + switch deviceOrientation {
  251 + case .portrait:
  252 + return position == .front ? .leftMirrored : .right
  253 + case .landscapeLeft:
  254 + return position == .front ? .downMirrored : .up
  255 + case .portraitUpsideDown:
  256 + return position == .front ? .rightMirrored : .left
  257 + case .landscapeRight:
  258 + return position == .front ? .upMirrored : .down
  259 + case .faceDown, .faceUp, .unknown:
  260 + return .up
  261 + @unknown default:
  262 + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back)
  263 + }
  264 + }
  265 +
  266 + /// Sends output of OutputBuffer to a Flutter texture
  267 + public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
  268 + if latestBuffer == nil {
  269 + return nil
  270 + }
  271 + return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
  272 + }
  273 +
  274 +}
  275 +
  1 +//
  2 +// MobileScannerError.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 24/08/2022.
  6 +//
  7 +import Foundation
  8 +
  9 +enum MobileScannerError: Error {
  10 + case noCamera
  11 + case alreadyStarted
  12 + case alreadyStopped
  13 + case torchError(_ error: Error)
  14 + case cameraError(_ error: Error)
  15 + case torchWhenStopped
  16 + case analyzerError(_ error: Error)
  17 +}
1 -//  
2 -// SwiftMobileScanner.swift  
3 -// mobile_scanner  
4 -//  
5 -// Created by Julian Steenbakker on 15/02/2022.  
6 -//  
7 -  
8 -import Foundation  
1 -import AVFoundation  
2 import Flutter 1 import Flutter
3 import MLKitVision 2 import MLKitVision
4 import MLKitBarcodeScanning 3 import MLKitBarcodeScanning
  4 +import AVFoundation
5 5
6 -public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {  
7 -  
8 - let registry: FlutterTextureRegistry  
9 -  
10 - // Sink for publishing event changes  
11 - var sink: FlutterEventSink! 6 +public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
12 7
13 - // Texture id of the camera preview  
14 - var textureId: Int64! 8 + /// The mobile scanner object that handles all logic
  9 + private let mobileScanner: MobileScanner
15 10
16 - // Capture session of the camera  
17 - var captureSession: AVCaptureSession! 11 + /// The handler sends all information via an event channel back to Flutter
  12 + private let barcodeHandler: BarcodeHandler
18 13
19 - // The selected camera  
20 - var device: AVCaptureDevice!  
21 -  
22 - // Image to be sent to the texture  
23 - var latestBuffer: CVImageBuffer! 14 + init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
  15 + self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
  16 + if barcodes != nil {
  17 + let barcodesMap = barcodes!.map { barcode in
  18 + return barcode.data
  19 + }
  20 + if (!barcodesMap.isEmpty) {
  21 + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)])
  22 + }
  23 + } else if (error != nil){
  24 + barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
  25 + }
  26 + })
  27 + self.barcodeHandler = barcodeHandler
  28 + super.init()
  29 + }
24 30
25 - // Return image buffer with the Barcode event  
26 - var returnImage: Bool = false  
27 -  
28 -// var analyzeMode: Int = 0  
29 - var analyzing: Bool = false  
30 - var position = AVCaptureDevice.Position.back  
31 -  
32 - var scanner = BarcodeScanner.barcodeScanner()  
33 -  
34 public static func register(with registrar: FlutterPluginRegistrar) { 31 public static func register(with registrar: FlutterPluginRegistrar) {
35 - let instance = SwiftMobileScannerPlugin(registrar.textures())  
36 -  
37 - let method = FlutterMethodChannel(name:  
38 - "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())  
39 - let event = FlutterEventChannel(name:  
40 - "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())  
41 - registrar.addMethodCallDelegate(instance, channel: method)  
42 - event.setStreamHandler(instance) 32 + let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures())
  33 + let methodChannel = FlutterMethodChannel(name:
  34 + "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
  35 + registrar.addMethodCallDelegate(instance, channel: methodChannel)
43 } 36 }
44 37
45 - init(_ registry: FlutterTextureRegistry) {  
46 - self.registry = registry  
47 - super.init()  
48 - }  
49 -  
50 -  
51 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 38 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
52 switch call.method { 39 switch call.method {
53 case "state": 40 case "state":
54 - checkPermission(call, result) 41 + result(mobileScanner.checkPermission())
55 case "request": 42 case "request":
56 - requestPermission(call, result) 43 + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
57 case "start": 44 case "start":
58 start(call, result) 45 start(call, result)
59 - case "torch":  
60 - toggleTorch(call, result)  
61 -// case "analyze":  
62 -// switchAnalyzeMode(call, result)  
63 case "stop": 46 case "stop":
64 stop(result) 47 stop(result)
  48 + case "torch":
  49 + toggleTorch(call, result)
65 case "analyzeImage": 50 case "analyzeImage":
66 analyzeImage(call, result) 51 analyzeImage(call, result)
67 -  
68 default: 52 default:
69 result(FlutterMethodNotImplemented) 53 result(FlutterMethodNotImplemented)
70 } 54 }
71 } 55 }
72 56
73 - // FlutterStreamHandler  
74 - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {  
75 - sink = events  
76 - return nil  
77 - }  
78 -  
79 - // FlutterStreamHandler  
80 - public func onCancel(withArguments arguments: Any?) -> FlutterError? {  
81 - sink = nil  
82 - return nil  
83 - }  
84 -  
85 - // FlutterTexture  
86 - public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {  
87 - if latestBuffer == nil {  
88 - return nil  
89 - }  
90 - return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)  
91 - }  
92 -  
93 - private func ciImageToJpeg(ciImage: CIImage) -> Data {  
94 -  
95 - // let ciImage = CIImage(cvPixelBuffer: latestBuffer)  
96 - let context:CIContext = CIContext.init(options: nil)  
97 - let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!  
98 - let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)  
99 -  
100 - return uiImage.jpegData(compressionQuality: 0.8)!;  
101 - }  
102 -  
103 - // Gets called when a new image is added to the buffer  
104 - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 57 + /// Parses all parameters and starts the mobileScanner
  58 + private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  59 + // let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int
  60 + let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
  61 + let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
  62 + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
  63 + let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
105 64
106 - latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
107 - registry.textureFrameAvailable(textureId) 65 + let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
  66 + var barcodeOptions: BarcodeScannerOptions? = nil
  67 +
  68 + if (formatList.count != 0) {
  69 + var barcodeFormats: BarcodeFormat = []
  70 + for index in formats {
  71 + barcodeFormats.insert(BarcodeFormat(rawValue: index))
  72 + }
  73 + barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)
  74 + }
108 75
109 -// switch analyzeMode {  
110 -// case 1: // barcode  
111 - if analyzing {  
112 - return  
113 - }  
114 - analyzing = true  
115 - let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
116 - let image = VisionImage(image: buffer!.image)  
117 - image.orientation = imageOrientation(  
118 - deviceOrientation: UIDevice.current.orientation,  
119 - defaultOrientation: .portrait  
120 - )  
121 76
122 - scanner.process(image) { [self] barcodes, error in  
123 - if error == nil && barcodes != nil {  
124 - for barcode in barcodes! {  
125 -  
126 - var event: [String: Any?] = ["name": "barcode", "data": barcode.data]  
127 - if (returnImage && latestBuffer != nil) {  
128 - let image: CIImage = CIImage(cvPixelBuffer: latestBuffer) 77 + let position = facing == 0 ? AVCaptureDevice.Position.front : .back
  78 + let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)!
129 79
130 - event["image"] = FlutterStandardTypedData(bytes: ciImageToJpeg(ciImage: image))  
131 - }  
132 - sink?(event)  
133 - }  
134 - }  
135 - analyzing = false  
136 - }  
137 -// default: // none  
138 -// break  
139 -// }  
140 - }  
141 -  
142 - func imageOrientation(  
143 - deviceOrientation: UIDeviceOrientation,  
144 - defaultOrientation: UIDeviceOrientation  
145 - ) -> UIImage.Orientation {  
146 - switch deviceOrientation {  
147 - case .portrait:  
148 - return position == .front ? .leftMirrored : .right  
149 - case .landscapeLeft:  
150 - return position == .front ? .downMirrored : .up  
151 - case .portraitUpsideDown:  
152 - return position == .front ? .rightMirrored : .left  
153 - case .landscapeRight:  
154 - return position == .front ? .upMirrored : .down  
155 - case .faceDown, .faceUp, .unknown:  
156 - return .up  
157 - @unknown default:  
158 - return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait)  
159 - }  
160 - }  
161 -  
162 - func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
163 - let status = AVCaptureDevice.authorizationStatus(for: .video)  
164 - switch status {  
165 - case .notDetermined:  
166 - result(0)  
167 - case .authorized:  
168 - result(1)  
169 - default:  
170 - result(2)  
171 - }  
172 - }  
173 -  
174 - func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
175 - AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })  
176 - }  
177 -  
178 - func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
179 - if (device != nil) { 80 + do {
  81 + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed)
  82 + result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
  83 + } catch MobileScannerError.alreadyStarted {
180 result(FlutterError(code: "MobileScanner", 84 result(FlutterError(code: "MobileScanner",
181 - message: "Called start() while already started!",  
182 - details: nil))  
183 - return  
184 - }  
185 -  
186 - textureId = registry.register(self)  
187 - captureSession = AVCaptureSession()  
188 -  
189 - let argReader = MapArgumentReader(call.arguments as? [String: Any])  
190 -  
191 - returnImage = argReader.bool(key: "returnImage") ?? false  
192 -  
193 -// let ratio: Int = argReader.int(key: "ratio")  
194 - let torch: Bool = argReader.bool(key: "torch") ?? false  
195 - let facing: Int = argReader.int(key: "facing") ?? 1  
196 - let formats: Array = argReader.intArray(key: "formats") ?? []  
197 -  
198 - if (formats.count != 0) {  
199 - var barcodeFormats: BarcodeFormat = []  
200 - for index in formats {  
201 - barcodeFormats.insert(BarcodeFormat(rawValue: index))  
202 - }  
203 -  
204 - let barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)  
205 - scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions)  
206 - }  
207 -  
208 - // Set the camera to use  
209 - position = facing == 0 ? AVCaptureDevice.Position.front : .back  
210 -  
211 - // Open the camera device  
212 - if #available(iOS 10.0, *) {  
213 - device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first  
214 - } else {  
215 - device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first  
216 - }  
217 -  
218 - if (device == nil) { 85 + message: "Called start() while already started!",
  86 + details: nil))
  87 + } catch MobileScannerError.noCamera {
219 result(FlutterError(code: "MobileScanner", 88 result(FlutterError(code: "MobileScanner",
220 - message: "No camera found or failed to open camera!",  
221 - details: nil))  
222 - return  
223 - }  
224 -  
225 - // Enable the torch if parameter is set and torch is available  
226 - if (device.hasTorch && device.isTorchAvailable) {  
227 - do {  
228 - try device.lockForConfiguration()  
229 - device.torchMode = torch ? .on : .off  
230 - device.unlockForConfiguration() 89 + message: "No camera found or failed to open camera!",
  90 + details: nil))
  91 + } catch MobileScannerError.torchError(let error) {
  92 + result(FlutterError(code: "MobileScanner",
  93 + message: "Error occured when setting toch!",
  94 + details: error))
  95 + } catch MobileScannerError.cameraError(let error) {
  96 + result(FlutterError(code: "MobileScanner",
  97 + message: "Error occured when setting up camera!",
  98 + details: error))
231 } catch { 99 } catch {
232 - error.throwNative(result)  
233 - } 100 + result(FlutterError(code: "MobileScanner",
  101 + message: "Unknown error occured..",
  102 + details: nil))
234 } 103 }
235 -  
236 - device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)  
237 - captureSession.beginConfiguration()  
238 -  
239 - // Add device input 104 + }
  105 +
  106 + /// Stops the mobileScanner and closes the texture
  107 + private func stop(_ result: @escaping FlutterResult) {
240 do { 108 do {
241 - let input = try AVCaptureDeviceInput(device: device)  
242 - captureSession.addInput(input) 109 + try mobileScanner.stop()
243 } catch { 110 } catch {
244 - error.throwNative(result)  
245 - }  
246 - captureSession.sessionPreset = AVCaptureSession.Preset.photo;  
247 - // Add video output.  
248 - let videoOutput = AVCaptureVideoDataOutput()  
249 -  
250 - videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]  
251 - videoOutput.alwaysDiscardsLateVideoFrames = true  
252 -  
253 - videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)  
254 - captureSession.addOutput(videoOutput)  
255 - for connection in videoOutput.connections {  
256 - connection.videoOrientation = .portrait  
257 - if position == .front && connection.isVideoMirroringSupported {  
258 - connection.isVideoMirrored = true  
259 - }  
260 - }  
261 - captureSession.commitConfiguration()  
262 - captureSession.startRunning()  
263 - let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)  
264 - let width = Double(demensions.height)  
265 - let height = Double(demensions.width)  
266 - let size = ["width": width, "height": height]  
267 - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]  
268 - result(answer)  
269 - }  
270 -  
271 - func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
272 - if (device == nil) {  
273 result(FlutterError(code: "MobileScanner", 111 result(FlutterError(code: "MobileScanner",
274 - message: "Called toggleTorch() while stopped!",  
275 - details: nil))  
276 - return 112 + message: "Called stop() while already stopped!",
  113 + details: nil))
277 } 114 }
  115 + result(nil)
  116 + }
  117 +
  118 + /// Toggles the torch
  119 + private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
278 do { 120 do {
279 - try device.lockForConfiguration()  
280 - device.torchMode = call.arguments as! Int == 1 ? .on : .off  
281 - device.unlockForConfiguration()  
282 - result(nil) 121 + try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)
283 } catch { 122 } catch {
284 - error.throwNative(result) 123 + result(FlutterError(code: "MobileScanner",
  124 + message: "Called toggleTorch() while stopped!",
  125 + details: nil))
285 } 126 }
  127 + result(nil)
286 } 128 }
287 129
288 -// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
289 -// analyzeMode = call.arguments as! Int  
290 -// result(nil)  
291 -// }  
292 -  
293 - func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
294 - let uiImage = UIImage(contentsOfFile: call.arguments as! String) 130 + /// Analyzes a single image
  131 + private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  132 + let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
295 133
296 if (uiImage == nil) { 134 if (uiImage == nil) {
297 result(FlutterError(code: "MobileScanner", 135 result(FlutterError(code: "MobileScanner",
298 - message: "No image found in analyzeImage!",  
299 - details: nil)) 136 + message: "No image found in analyzeImage!",
  137 + details: nil))
300 return 138 return
301 } 139 }
302 -  
303 - let image = VisionImage(image: uiImage!)  
304 - image.orientation = imageOrientation(  
305 - deviceOrientation: UIDevice.current.orientation,  
306 - defaultOrientation: .portrait  
307 - )  
308 -  
309 - var barcodeFound = false  
310 -  
311 - scanner.process(image) { [self] barcodes, error in 140 + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in
312 if error == nil && barcodes != nil { 141 if error == nil && barcodes != nil {
313 for barcode in barcodes! { 142 for barcode in barcodes! {
314 - barcodeFound = true  
315 let event: [String: Any?] = ["name": "barcode", "data": barcode.data] 143 let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
316 - sink?(event) 144 + barcodeHandler.publishEvent(event)
317 } 145 }
318 } else if error != nil { 146 } else if error != nil {
319 - result(FlutterError(code: "MobileScanner",  
320 - message: error?.localizedDescription,  
321 - details: "analyzeImage()")) 147 + barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])
322 } 148 }
323 - analyzing = false  
324 - result(barcodeFound)  
325 - }  
326 -  
327 - }  
328 -  
329 - func stop(_ result: FlutterResult) {  
330 - if (device == nil) {  
331 - result(FlutterError(code: "MobileScanner",  
332 - message: "Called stop() while already stopped!",  
333 - details: nil))  
334 - return  
335 - }  
336 - captureSession.stopRunning()  
337 - for input in captureSession.inputs {  
338 - captureSession.removeInput(input)  
339 - }  
340 - for output in captureSession.outputs {  
341 - captureSession.removeOutput(output)  
342 - }  
343 - device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))  
344 - registry.unregisterTexture(textureId)  
345 -  
346 -// analyzeMode = 0  
347 - latestBuffer = nil  
348 - captureSession = nil  
349 - device = nil  
350 - textureId = nil  
351 - 149 + })
352 result(nil) 150 result(nil)
353 } 151 }
354 152
355 - // Observer for torch state 153 + /// Observer for torch state
356 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 154 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
357 switch keyPath { 155 switch keyPath {
358 case "torchMode": 156 case "torchMode":
359 // off = 0; on = 1; auto = 2; 157 // off = 0; on = 1; auto = 2;
360 let state = change?[.newKey] as? Int 158 let state = change?[.newKey] as? Int
361 - let event: [String: Any?] = ["name": "torchState", "data": state]  
362 - sink?(event) 159 + barcodeHandler.publishEvent(["name": "torchState", "data": state])
363 default: 160 default:
364 break 161 break
365 } 162 }
366 } 163 }
367 } 164 }
368 165
369 -class MapArgumentReader {  
370 -  
371 - let args: [String: Any]?  
372 -  
373 - init(_ args: [String: Any]?) {  
374 - self.args = args  
375 - }  
376 -  
377 - func string(key: String) -> String? {  
378 - return args?[key] as? String  
379 - }  
380 -  
381 - func int(key: String) -> Int? {  
382 - return (args?[key] as? NSNumber)?.intValue  
383 - }  
384 -  
385 - func bool(key: String) -> Bool? {  
386 - return (args?[key] as? NSNumber)?.boolValue  
387 - }  
388 -  
389 - func stringArray(key: String) -> [String]? {  
390 - return args?[key] as? [String]  
391 - }  
392 -  
393 - func intArray(key: String) -> [Int]? {  
394 - return args?[key] as? [Int]  
395 - }  
396 - 166 +enum DetectionSpeed: Int {
  167 + case noDuplicates = 0
  168 + case normal = 1
  169 + case unrestricted = 2
397 } 170 }
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '0.0.1' 7 + s.version = '3.0.0'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -16,7 +16,7 @@ An universal scanner for Flutter based on MLKit. @@ -16,7 +16,7 @@ An universal scanner for Flutter based on MLKit.
16 s.source_files = 'Classes/**/*' 16 s.source_files = 'Classes/**/*'
17 s.dependency 'Flutter' 17 s.dependency 'Flutter'
18 s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0' 18 s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0'
19 - s.platform = :ios, '10.0' 19 + s.platform = :ios, '11.0'
20 s.static_framework = true 20 s.static_framework = true
21 # Flutter.framework does not contain a i386 slice. 21 # Flutter.framework does not contain a i386 slice.
22 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
1 library mobile_scanner; 1 library mobile_scanner;
2 2
  3 +export 'src/barcode.dart';
  4 +export 'src/barcode_capture.dart';
  5 +export 'src/enums/camera_facing.dart';
  6 +export 'src/enums/detection_speed.dart';
  7 +export 'src/enums/mobile_scanner_state.dart';
  8 +export 'src/enums/ratio.dart';
  9 +export 'src/enums/torch_state.dart';
3 export 'src/mobile_scanner.dart'; 10 export 'src/mobile_scanner.dart';
4 export 'src/mobile_scanner_arguments.dart'; 11 export 'src/mobile_scanner_arguments.dart';
5 export 'src/mobile_scanner_controller.dart'; 12 export 'src/mobile_scanner_controller.dart';
6 -export 'src/objects/barcode.dart';  
@@ -5,7 +5,7 @@ import 'dart:ui' as ui; @@ -5,7 +5,7 @@ import 'dart:ui' as ui;
5 import 'package:flutter/material.dart'; 5 import 'package:flutter/material.dart';
6 import 'package:flutter/services.dart'; 6 import 'package:flutter/services.dart';
7 import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 7 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
8 -import 'package:mobile_scanner/mobile_scanner.dart'; 8 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
9 import 'package:mobile_scanner/src/web/jsqr.dart'; 9 import 'package:mobile_scanner/src/web/jsqr.dart';
10 import 'package:mobile_scanner/src/web/media.dart'; 10 import 'package:mobile_scanner/src/web/media.dart';
11 11
1 import 'dart:typed_data'; 1 import 'dart:typed_data';
2 import 'dart:ui'; 2 import 'dart:ui';
3 3
4 -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; 4 +import 'package:mobile_scanner/src/barcode_utility.dart';
5 5
6 /// Represents a single recognized barcode and its value. 6 /// Represents a single recognized barcode and its value.
7 class Barcode { 7 class Barcode {
@@ -97,7 +97,7 @@ class Barcode { @@ -97,7 +97,7 @@ class Barcode {
97 }); 97 });
98 98
99 /// Create a [Barcode] from native data. 99 /// Create a [Barcode] from native data.
100 - Barcode.fromNative(Map data, this.image) 100 + Barcode.fromNative(Map data, {this.image})
101 : corners = toCorners(data['corners'] as List?), 101 : corners = toCorners(data['corners'] as List?),
102 format = toFormat(data['format'] as int), 102 format = toFormat(data['format'] as int),
103 rawBytes = data['rawBytes'] as Uint8List?, 103 rawBytes = data['rawBytes'] as Uint8List?,
  1 +import 'dart:typed_data';
  2 +
  3 +import 'package:mobile_scanner/src/barcode.dart';
  4 +
  5 +class BarcodeCapture {
  6 + List<Barcode> barcodes;
  7 + Uint8List? image;
  8 +
  9 + BarcodeCapture({
  10 + required this.barcodes,
  11 + this.image,
  12 + });
  13 +}
  1 +/// The facing of a camera.
  2 +enum CameraFacing {
  3 + /// Front facing camera.
  4 + front,
  5 +
  6 + /// Back facing camera.
  7 + back,
  8 +}
  1 +/// The detection speed of the scanner.
  2 +enum DetectionSpeed {
  3 + /// The scanner will only scan a barcode once, and never again until another
  4 + /// barcode has been scanned.
  5 + noDuplicates,
  6 +
  7 + /// Front facing camera.
  8 + normal,
  9 +
  10 + /// Back facing camera.
  11 + unrestricted,
  12 +}
  1 +enum MobileScannerState { undetermined, authorized, denied }
  1 +enum Ratio { ratio_4_3, ratio_16_9 }
  1 +/// The state of torch.
  2 +enum TorchState {
  3 + /// Torch is off.
  4 + off,
  5 +
  6 + /// Torch is on.
  7 + on,
  8 +}
1 import 'package:flutter/foundation.dart'; 1 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
3 -import 'package:mobile_scanner/mobile_scanner.dart';  
4 -  
5 -enum Ratio { ratio_4_3, ratio_16_9 } 3 +import 'package:mobile_scanner/src/barcode_capture.dart';
  4 +import 'package:mobile_scanner/src/mobile_scanner_arguments.dart';
  5 +import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
6 6
7 /// A widget showing a live camera preview. 7 /// A widget showing a live camera preview.
8 class MobileScanner extends StatefulWidget { 8 class MobileScanner extends StatefulWidget {
@@ -15,28 +15,23 @@ class MobileScanner extends StatefulWidget { @@ -15,28 +15,23 @@ class MobileScanner extends StatefulWidget {
15 /// Function that gets called when a Barcode is detected. 15 /// Function that gets called when a Barcode is detected.
16 /// 16 ///
17 /// [barcode] The barcode object with all information about the scanned code. 17 /// [barcode] The barcode object with all information about the scanned code.
18 - /// [args] Information about the state of the MobileScanner widget  
19 - final Function(Barcode barcode, MobileScannerArguments? args) onDetect;  
20 -  
21 - /// TODO: Function that gets called when the Widget is initialized. Can be usefull  
22 - /// to check wether the device has a torch(flash) or not.  
23 - ///  
24 - /// [args] Information about the state of the MobileScanner widget  
25 - // final Function(MobileScannerArguments args)? onInitialize; 18 + /// [startArguments] Information about the state of the MobileScanner widget
  19 + final Function(BarcodeCapture capture, MobileScannerArguments? arguments)
  20 + onDetect;
26 21
27 /// Handles how the widget should fit the screen. 22 /// Handles how the widget should fit the screen.
28 final BoxFit fit; 23 final BoxFit fit;
29 24
30 - /// Set to false if you don't want duplicate scans.  
31 - final bool allowDuplicates; 25 + /// Whether to automatically resume the camera when the application is resumed
  26 + final bool autoResume;
32 27
33 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. 28 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
34 const MobileScanner({ 29 const MobileScanner({
35 super.key, 30 super.key,
36 required this.onDetect, 31 required this.onDetect,
37 this.controller, 32 this.controller,
  33 + this.autoResume = true,
38 this.fit = BoxFit.cover, 34 this.fit = BoxFit.cover,
39 - this.allowDuplicates = false,  
40 this.onPermissionSet, 35 this.onPermissionSet,
41 }); 36 });
42 37
@@ -57,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner> @@ -57,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner>
57 if (!controller.isStarting) controller.start(); 52 if (!controller.isStarting) controller.start();
58 } 53 }
59 54
  55 + AppLifecycleState? _lastState;
  56 +
60 @override 57 @override
61 void didChangeAppLifecycleState(AppLifecycleState state) { 58 void didChangeAppLifecycleState(AppLifecycleState state) {
62 switch (state) { 59 switch (state) {
63 case AppLifecycleState.resumed: 60 case AppLifecycleState.resumed:
64 - if (!controller.isStarting && controller.autoResume) controller.start(); 61 + if (!controller.isStarting &&
  62 + widget.autoResume &&
  63 + _lastState != AppLifecycleState.inactive) controller.start();
65 break; 64 break;
66 - case AppLifecycleState.inactive:  
67 case AppLifecycleState.paused: 65 case AppLifecycleState.paused:
68 case AppLifecycleState.detached: 66 case AppLifecycleState.detached:
69 controller.stop(); 67 controller.stop();
70 break; 68 break;
  69 + default:
  70 + break;
71 } 71 }
  72 + _lastState = state;
72 } 73 }
73 74
74 - Uint8List? lastScanned;  
75 -  
76 @override 75 @override
77 Widget build(BuildContext context) { 76 Widget build(BuildContext context) {
78 return ValueListenableBuilder( 77 return ValueListenableBuilder(
79 - valueListenable: controller.args, 78 + valueListenable: controller.startArguments,
80 builder: (context, value, child) { 79 builder: (context, value, child) {
81 value = value as MobileScannerArguments?; 80 value = value as MobileScannerArguments?;
82 if (value == null) { 81 if (value == null) {
83 return const ColoredBox(color: Colors.black); 82 return const ColoredBox(color: Colors.black);
84 } else { 83 } else {
85 controller.barcodes.listen((barcode) { 84 controller.barcodes.listen((barcode) {
86 - if (!widget.allowDuplicates) {  
87 - if (lastScanned != barcode.rawBytes) {  
88 - lastScanned = barcode.rawBytes;  
89 - widget.onDetect(barcode, value! as MobileScannerArguments);  
90 - }  
91 - } else {  
92 - widget.onDetect(barcode, value! as MobileScannerArguments);  
93 - } 85 + widget.onDetect(barcode, value! as MobileScannerArguments);
94 }); 86 });
95 return ClipRect( 87 return ClipRect(
96 child: SizedBox( 88 child: SizedBox(
@@ -5,192 +5,155 @@ import 'package:flutter/cupertino.dart'; @@ -5,192 +5,155 @@ import 'package:flutter/cupertino.dart';
5 import 'package:flutter/foundation.dart'; 5 import 'package:flutter/foundation.dart';
6 import 'package:flutter/services.dart'; 6 import 'package:flutter/services.dart';
7 import 'package:mobile_scanner/mobile_scanner.dart'; 7 import 'package:mobile_scanner/mobile_scanner.dart';
8 -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; 8 +import 'package:mobile_scanner/src/barcode_utility.dart';
  9 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
9 10
10 -/// The facing of a camera.  
11 -enum CameraFacing {  
12 - /// Front facing camera.  
13 - front, 11 +/// The [MobileScannerController] holds all the logic of this plugin,
  12 +/// where as the [MobileScanner] class is the frontend of this plugin.
  13 +class MobileScannerController {
  14 + MobileScannerController({
  15 + this.facing = CameraFacing.back,
  16 + this.detectionSpeed = DetectionSpeed.noDuplicates,
  17 + // this.ratio,
  18 + this.torchEnabled = false,
  19 + this.formats,
  20 + // this.autoResume = true,
  21 + this.returnImage = false,
  22 + this.onPermissionSet,
  23 + }) {
  24 + // In case a new instance is created before calling dispose()
  25 + if (controllerHashcode != null) {
  26 + stop();
  27 + }
  28 + controllerHashcode = hashCode;
  29 + events = _eventChannel
  30 + .receiveBroadcastStream()
  31 + .listen((data) => _handleEvent(data as Map));
  32 + }
14 33
15 - /// Back facing camera.  
16 - back,  
17 -} 34 + //Must be static to keep the same value on new instances
  35 + static int? controllerHashcode;
  36 +
  37 + /// Select which camera should be used.
  38 + ///
  39 + /// Default: CameraFacing.back
  40 + final CameraFacing facing;
18 41
19 -enum MobileScannerState { undetermined, authorized, denied } 42 + // /// Analyze the image in 4:3 or 16:9
  43 + // ///
  44 + // /// Only on Android
  45 + // final Ratio? ratio;
20 46
21 -/// The state of torch.  
22 -enum TorchState {  
23 - /// Torch is off.  
24 - off, 47 + /// Enable or disable the torch (Flash) on start
  48 + ///
  49 + /// Default: disabled
  50 + final bool torchEnabled;
25 51
26 - /// Torch is on.  
27 - on,  
28 -} 52 + /// Set to true if you want to return the image buffer with the Barcode event
  53 + ///
  54 + /// Only supported on iOS and Android
  55 + final bool returnImage;
29 56
30 -// enum AnalyzeMode { none, barcode } 57 + /// If provided, the scanner will only detect those specific formats
  58 + final List<BarcodeFormat>? formats;
31 59
32 -class MobileScannerController {  
33 - MethodChannel methodChannel =  
34 - const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');  
35 - EventChannel eventChannel =  
36 - const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); 60 + /// Sets the speed of detections.
  61 + ///
  62 + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
  63 + final DetectionSpeed detectionSpeed;
37 64
38 - //Must be static to keep the same value on new instances  
39 - static int? _controllerHashcode;  
40 - StreamSubscription? events; 65 + /// Sets the barcode stream
  66 + final StreamController<BarcodeCapture> _barcodesController =
  67 + StreamController.broadcast();
  68 + Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
  69 +
  70 + static const MethodChannel _methodChannel =
  71 + MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
  72 + static const EventChannel _eventChannel =
  73 + EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
41 74
42 Function(bool permissionGranted)? onPermissionSet; 75 Function(bool permissionGranted)? onPermissionSet;
43 - final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null);  
44 - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);  
45 - late final ValueNotifier<CameraFacing> cameraFacingState;  
46 - final Ratio? ratio;  
47 - final bool? torchEnabled;  
48 - // Whether to return the image buffer with the Barcode event  
49 - final bool returnImage;  
50 76
51 - /// If provided, the scanner will only detect those specific formats.  
52 - final List<BarcodeFormat>? formats; 77 + /// Listen to events from the platform specific code
  78 + late StreamSubscription events;
53 79
54 - CameraFacing facing;  
55 - bool hasTorch = false;  
56 - late StreamController<Barcode> barcodesController; 80 + /// A notifier that provides several arguments about the MobileScanner
  81 + final ValueNotifier<MobileScannerArguments?> startArguments =
  82 + ValueNotifier(null);
57 83
58 - /// Whether to automatically resume the camera when the application is resumed  
59 - bool autoResume; 84 + /// A notifier that provides the state of the Torch (Flash)
  85 + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
60 86
61 - Stream<Barcode> get barcodes => barcodesController.stream; 87 + /// A notifier that provides the state of which camera is being used
  88 + late final ValueNotifier<CameraFacing> cameraFacingState =
  89 + ValueNotifier(facing);
62 90
63 - MobileScannerController({  
64 - this.facing = CameraFacing.back,  
65 - this.ratio,  
66 - this.torchEnabled,  
67 - this.formats,  
68 - this.onPermissionSet,  
69 - this.autoResume = true,  
70 - this.returnImage = false,  
71 - }) {  
72 - // In case a new instance is created before calling dispose()  
73 - if (_controllerHashcode != null) {  
74 - stop();  
75 - }  
76 - _controllerHashcode = hashCode; 91 + bool isStarting = false;
  92 + bool? _hasTorch;
77 93
78 - cameraFacingState = ValueNotifier(facing); 94 + /// Set the starting arguments for the camera
  95 + Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
  96 + final Map<String, dynamic> arguments = {};
79 97
80 - // Sets analyze mode and barcode stream  
81 - barcodesController = StreamController.broadcast(  
82 - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),  
83 - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),  
84 - ); 98 + cameraFacingState.value = cameraFacingOverride ?? facing;
  99 + arguments['facing'] = cameraFacingState.value.index;
85 100
86 - // Listen to events from the platform specific code  
87 - events = eventChannel  
88 - .receiveBroadcastStream()  
89 - .listen((data) => handleEvent(data as Map));  
90 - } 101 + // if (ratio != null) arguments['ratio'] = ratio;
  102 + arguments['torch'] = torchEnabled;
  103 + arguments['speed'] = detectionSpeed.index;
91 104
92 - void handleEvent(Map event) {  
93 - final name = event['name'];  
94 - final data = event['data'];  
95 - final binaryData = event['binaryData'];  
96 - switch (name) {  
97 - case 'torchState':  
98 - final state = TorchState.values[data as int? ?? 0];  
99 - torchState.value = state;  
100 - break;  
101 - case 'barcode':  
102 - final image = returnImage ? event['image'] as Uint8List : null;  
103 - final barcode = Barcode.fromNative(data as Map? ?? {}, image);  
104 - barcodesController.add(barcode);  
105 - break;  
106 - case 'barcodeMac':  
107 - barcodesController.add(  
108 - Barcode(  
109 - rawValue: (data as Map)['payload'] as String?,  
110 - ),  
111 - );  
112 - break;  
113 - case 'barcodeWeb':  
114 - final bytes = (binaryData as List).cast<int>();  
115 - barcodesController.add(  
116 - Barcode(  
117 - rawValue: data as String?,  
118 - rawBytes: Uint8List.fromList(bytes),  
119 - ),  
120 - );  
121 - break;  
122 - default:  
123 - throw UnimplementedError(); 105 + if (formats != null) {
  106 + if (Platform.isAndroid) {
  107 + arguments['formats'] = formats!.map((e) => e.index).toList();
  108 + } else if (Platform.isIOS || Platform.isMacOS) {
  109 + arguments['formats'] = formats!.map((e) => e.rawValue).toList();
  110 + }
124 } 111 }
  112 + arguments['returnImage'] = true;
  113 + return arguments;
125 } 114 }
126 115
127 - // TODO: Add more analyzers like text analyzer  
128 - // void setAnalyzeMode(int mode) {  
129 - // if (hashCode != _controllerHashcode) {  
130 - // return;  
131 - // }  
132 - // methodChannel.invokeMethod('analyze', mode);  
133 - // }  
134 -  
135 - // List<BarcodeFormats>? formats = _defaultBarcodeFormats,  
136 - bool isStarting = false;  
137 -  
138 /// Start barcode scanning. This will first check if the required permissions 116 /// Start barcode scanning. This will first check if the required permissions
139 /// are set. 117 /// are set.
140 - Future<void> start() async {  
141 - ensure('startAsync'); 118 + Future<MobileScannerArguments?> start({
  119 + CameraFacing? cameraFacingOverride,
  120 + }) async {
  121 + debugPrint('Hashcode controller: $hashCode');
142 if (isStarting) { 122 if (isStarting) {
143 - throw Exception('mobile_scanner: Called start() while already starting.'); 123 + debugPrint("Called start() while starting.");
144 } 124 }
145 isStarting = true; 125 isStarting = true;
146 - // setAnalyzeMode(AnalyzeMode.barcode.index);  
147 126
148 // Check authorization status 127 // Check authorization status
149 if (!kIsWeb) { 128 if (!kIsWeb) {
150 - MobileScannerState state = MobileScannerState  
151 - .values[await methodChannel.invokeMethod('state') as int? ?? 0]; 129 + final MobileScannerState state = MobileScannerState
  130 + .values[await _methodChannel.invokeMethod('state') as int? ?? 0];
152 switch (state) { 131 switch (state) {
153 case MobileScannerState.undetermined: 132 case MobileScannerState.undetermined:
154 final bool result = 133 final bool result =
155 - await methodChannel.invokeMethod('request') as bool? ?? false;  
156 - state = result  
157 - ? MobileScannerState.authorized  
158 - : MobileScannerState.denied;  
159 - onPermissionSet?.call(result); 134 + await _methodChannel.invokeMethod('request') as bool? ?? false;
  135 + if (!result) {
  136 + isStarting = false;
  137 + onPermissionSet?.call(result);
  138 + throw MobileScannerException('User declined camera permission.');
  139 + }
160 break; 140 break;
161 case MobileScannerState.denied: 141 case MobileScannerState.denied:
162 isStarting = false; 142 isStarting = false;
163 onPermissionSet?.call(false); 143 onPermissionSet?.call(false);
164 - throw PlatformException(code: 'NO ACCESS'); 144 + throw MobileScannerException('User declined camera permission.');
165 case MobileScannerState.authorized: 145 case MobileScannerState.authorized:
166 onPermissionSet?.call(true); 146 onPermissionSet?.call(true);
167 break; 147 break;
168 } 148 }
169 } 149 }
170 150
171 - cameraFacingState.value = facing;  
172 -  
173 - // Set the starting arguments for the camera  
174 - final Map arguments = {};  
175 - arguments['facing'] = facing.index;  
176 - if (ratio != null) arguments['ratio'] = ratio;  
177 - if (torchEnabled != null) arguments['torch'] = torchEnabled;  
178 -  
179 - if (formats != null) {  
180 - if (Platform.isAndroid) {  
181 - arguments['formats'] = formats!.map((e) => e.index).toList();  
182 - } else if (Platform.isIOS || Platform.isMacOS) {  
183 - arguments['formats'] = formats!.map((e) => e.rawValue).toList();  
184 - }  
185 - }  
186 - arguments['returnImage'] = returnImage;  
187 -  
188 // Start the camera with arguments 151 // Start the camera with arguments
189 Map<String, dynamic>? startResult = {}; 152 Map<String, dynamic>? startResult = {};
190 try { 153 try {
191 - startResult = await methodChannel.invokeMapMethod<String, dynamic>( 154 + startResult = await _methodChannel.invokeMapMethod<String, dynamic>(
192 'start', 155 'start',
193 - arguments, 156 + _argumentsToMap(cameraFacingOverride: cameraFacingOverride),
194 ); 157 );
195 } on PlatformException catch (error) { 158 } on PlatformException catch (error) {
196 debugPrint('${error.code}: ${error.message}'); 159 debugPrint('${error.code}: ${error.message}');
@@ -198,85 +161,78 @@ class MobileScannerController { @@ -198,85 +161,78 @@ class MobileScannerController {
198 if (error.code == "MobileScannerWeb") { 161 if (error.code == "MobileScannerWeb") {
199 onPermissionSet?.call(false); 162 onPermissionSet?.call(false);
200 } 163 }
201 - // setAnalyzeMode(AnalyzeMode.none.index);  
202 - return; 164 + return null;
203 } 165 }
204 166
205 if (startResult == null) { 167 if (startResult == null) {
206 isStarting = false; 168 isStarting = false;
207 - throw PlatformException(code: 'INITIALIZATION ERROR'); 169 + throw MobileScannerException(
  170 + 'Failed to start mobileScanner, no response from platform side',
  171 + );
208 } 172 }
209 173
210 - hasTorch = startResult['torchable'] as bool? ?? false; 174 + _hasTorch = startResult['torchable'] as bool? ?? false;
  175 + if (_hasTorch! && torchEnabled) {
  176 + torchState.value = TorchState.on;
  177 + }
211 178
212 if (kIsWeb) { 179 if (kIsWeb) {
213 onPermissionSet?.call( 180 onPermissionSet?.call(
214 true, 181 true,
215 ); // If we reach this line, it means camera permission has been granted 182 ); // If we reach this line, it means camera permission has been granted
216 183
217 - args.value = MobileScannerArguments( 184 + startArguments.value = MobileScannerArguments(
218 webId: startResult['ViewID'] as String?, 185 webId: startResult['ViewID'] as String?,
219 size: Size( 186 size: Size(
220 startResult['videoWidth'] as double? ?? 0, 187 startResult['videoWidth'] as double? ?? 0,
221 startResult['videoHeight'] as double? ?? 0, 188 startResult['videoHeight'] as double? ?? 0,
222 ), 189 ),
223 - hasTorch: hasTorch, 190 + hasTorch: _hasTorch!,
224 ); 191 );
225 } else { 192 } else {
226 - args.value = MobileScannerArguments( 193 + startArguments.value = MobileScannerArguments(
227 textureId: startResult['textureId'] as int?, 194 textureId: startResult['textureId'] as int?,
228 size: toSize(startResult['size'] as Map? ?? {}), 195 size: toSize(startResult['size'] as Map? ?? {}),
229 - hasTorch: hasTorch, 196 + hasTorch: _hasTorch!,
230 ); 197 );
231 } 198 }
232 -  
233 isStarting = false; 199 isStarting = false;
  200 + return startArguments.value!;
234 } 201 }
235 202
  203 + /// Stops the camera, but does not dispose this controller.
236 Future<void> stop() async { 204 Future<void> stop() async {
237 - try {  
238 - await methodChannel.invokeMethod('stop');  
239 - } on PlatformException catch (error) {  
240 - debugPrint('${error.code}: ${error.message}');  
241 - } 205 + await _methodChannel.invokeMethod('stop');
242 } 206 }
243 207
244 /// Switches the torch on or off. 208 /// Switches the torch on or off.
245 /// 209 ///
246 /// Only works if torch is available. 210 /// Only works if torch is available.
247 Future<void> toggleTorch() async { 211 Future<void> toggleTorch() async {
248 - ensure('toggleTorch');  
249 - if (!hasTorch) {  
250 - debugPrint('Device has no torch/flash.');  
251 - return; 212 + if (_hasTorch == null) {
  213 + throw MobileScannerException(
  214 + 'Cannot toggle torch if start() has never been called',
  215 + );
  216 + } else if (!_hasTorch!) {
  217 + throw MobileScannerException('Device has no torch');
252 } 218 }
253 219
254 - final TorchState state = 220 + torchState.value =
255 torchState.value == TorchState.off ? TorchState.on : TorchState.off; 221 torchState.value == TorchState.off ? TorchState.on : TorchState.off;
256 222
257 - try {  
258 - await methodChannel.invokeMethod('torch', state.index);  
259 - } on PlatformException catch (error) {  
260 - debugPrint('${error.code}: ${error.message}');  
261 - } 223 + await _methodChannel.invokeMethod('torch', torchState.value.index);
262 } 224 }
263 225
264 /// Switches the torch on or off. 226 /// Switches the torch on or off.
265 /// 227 ///
266 /// Only works if torch is available. 228 /// Only works if torch is available.
267 Future<void> switchCamera() async { 229 Future<void> switchCamera() async {
268 - ensure('switchCamera');  
269 - try {  
270 - await methodChannel.invokeMethod('stop');  
271 - } on PlatformException catch (error) {  
272 - debugPrint(  
273 - '${error.code}: camera is stopped! Please start before switching camera.',  
274 - );  
275 - return;  
276 - }  
277 - facing =  
278 - facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back;  
279 - await start(); 230 + await _methodChannel.invokeMethod('stop');
  231 + final CameraFacing facingToUse =
  232 + cameraFacingState.value == CameraFacing.back
  233 + ? CameraFacing.front
  234 + : CameraFacing.back;
  235 + await start(cameraFacingOverride: facingToUse);
280 } 236 }
281 237
282 /// Handles a local image file. 238 /// Handles a local image file.
@@ -285,28 +241,72 @@ class MobileScannerController { @@ -285,28 +241,72 @@ class MobileScannerController {
285 /// 241 ///
286 /// [path] The path of the image on the devices 242 /// [path] The path of the image on the devices
287 Future<bool> analyzeImage(String path) async { 243 Future<bool> analyzeImage(String path) async {
288 - return methodChannel 244 + return _methodChannel
289 .invokeMethod<bool>('analyzeImage', path) 245 .invokeMethod<bool>('analyzeImage', path)
290 .then<bool>((bool? value) => value ?? false); 246 .then<bool>((bool? value) => value ?? false);
291 } 247 }
292 248
293 /// Disposes the MobileScannerController and closes all listeners. 249 /// Disposes the MobileScannerController and closes all listeners.
  250 + ///
  251 + /// If you call this, you cannot use this controller object anymore.
294 void dispose() { 252 void dispose() {
295 - if (hashCode == _controllerHashcode) {  
296 - stop();  
297 - events?.cancel();  
298 - events = null;  
299 - _controllerHashcode = null; 253 + stop();
  254 + events.cancel();
  255 + _barcodesController.close();
  256 + if (hashCode == controllerHashcode) {
  257 + controllerHashcode = null;
300 onPermissionSet = null; 258 onPermissionSet = null;
301 } 259 }
302 - barcodesController.close();  
303 } 260 }
304 261
305 - /// Checks if the MobileScannerController is bound to the correct MobileScanner object.  
306 - void ensure(String name) {  
307 - final message =  
308 - 'MobileScannerController.$name called after MobileScannerController.dispose\n'  
309 - 'MobileScannerController methods should not be used after calling dispose.';  
310 - assert(hashCode == _controllerHashcode, message); 262 + /// Handles a returning event from the platform side
  263 + void _handleEvent(Map event) {
  264 + final name = event['name'];
  265 + final data = event['data'];
  266 +
  267 + switch (name) {
  268 + case 'torchState':
  269 + final state = TorchState.values[data as int? ?? 0];
  270 + torchState.value = state;
  271 + break;
  272 + case 'barcode':
  273 + if (data == null) return;
  274 + final parsed = (data as List)
  275 + .map((value) => Barcode.fromNative(value as Map))
  276 + .toList();
  277 + _barcodesController.add(
  278 + BarcodeCapture(
  279 + barcodes: parsed,
  280 + image: event['image'] as Uint8List,
  281 + ),
  282 + );
  283 + break;
  284 + case 'barcodeMac':
  285 + _barcodesController.add(
  286 + BarcodeCapture(
  287 + barcodes: [
  288 + Barcode(
  289 + rawValue: (data as Map)['payload'] as String?,
  290 + )
  291 + ],
  292 + ),
  293 + );
  294 + break;
  295 + case 'barcodeWeb':
  296 + _barcodesController.add(
  297 + BarcodeCapture(
  298 + barcodes: [
  299 + Barcode(
  300 + rawValue: data as String?,
  301 + )
  302 + ],
  303 + ),
  304 + );
  305 + break;
  306 + case 'error':
  307 + throw MobileScannerException(data as String);
  308 + default:
  309 + throw UnimplementedError(name as String?);
  310 + }
311 } 311 }
312 } 312 }
  1 +class MobileScannerException implements Exception {
  2 + String message;
  3 + MobileScannerException(this.message);
  4 +}
@@ -14,6 +14,7 @@ dependencies: @@ -14,6 +14,7 @@ dependencies:
14 sdk: flutter 14 sdk: flutter
15 js: ^0.6.3 15 js: ^0.6.3
16 16
  17 +
17 dev_dependencies: 18 dev_dependencies:
18 flutter_test: 19 flutter_test:
19 sdk: flutter 20 sdk: flutter