Julian Steenbakker
Committed by GitHub

Merge pull request #349 from juliansteenbakker/feature/return-image-dart

feat: add return image and refactor existing functions
@@ -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 +}
@@ -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 = (
@@ -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 library mobile_scanner; 1 library mobile_scanner;
2 2
  3 +export 'src/barcode.dart';
  4 +export 'src/barcode_capture.dart';
3 export 'src/enums/camera_facing.dart'; 5 export 'src/enums/camera_facing.dart';
4 export 'src/enums/detection_speed.dart'; 6 export 'src/enums/detection_speed.dart';
5 export 'src/enums/mobile_scanner_state.dart'; 7 export 'src/enums/mobile_scanner_state.dart';
@@ -8,4 +10,3 @@ export 'src/enums/torch_state.dart'; @@ -8,4 +10,3 @@ export 'src/enums/torch_state.dart';
8 export 'src/mobile_scanner.dart'; 10 export 'src/mobile_scanner.dart';
9 export 'src/mobile_scanner_arguments.dart'; 11 export 'src/mobile_scanner_arguments.dart';
10 export 'src/mobile_scanner_controller.dart'; 12 export 'src/mobile_scanner_controller.dart';
11 -export 'src/objects/barcode.dart';  
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 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'; 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';
4 6
5 /// A widget showing a live camera preview. 7 /// A widget showing a live camera preview.
6 class MobileScanner extends StatefulWidget { 8 class MobileScanner extends StatefulWidget {
@@ -13,28 +15,23 @@ class MobileScanner extends StatefulWidget { @@ -13,28 +15,23 @@ class MobileScanner extends StatefulWidget {
13 /// Function that gets called when a Barcode is detected. 15 /// Function that gets called when a Barcode is detected.
14 /// 16 ///
15 /// [barcode] The barcode object with all information about the scanned code. 17 /// [barcode] The barcode object with all information about the scanned code.
16 - /// [args] Information about the state of the MobileScanner widget  
17 - final Function(Barcode barcode, MobileScannerArguments? args) onDetect;  
18 -  
19 - /// TODO: Function that gets called when the Widget is initialized. Can be usefull  
20 - /// to check wether the device has a torch(flash) or not.  
21 - ///  
22 - /// [args] Information about the state of the MobileScanner widget  
23 - // final Function(MobileScannerArguments args)? onInitialize; 18 + /// [startArguments] Information about the state of the MobileScanner widget
  19 + final Function(BarcodeCapture capture, MobileScannerArguments? arguments)
  20 + onDetect;
24 21
25 /// Handles how the widget should fit the screen. 22 /// Handles how the widget should fit the screen.
26 final BoxFit fit; 23 final BoxFit fit;
27 24
28 - /// Set to false if you don't want duplicate scans.  
29 - final bool allowDuplicates; 25 + /// Whether to automatically resume the camera when the application is resumed
  26 + final bool autoResume;
30 27
31 /// 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.
32 const MobileScanner({ 29 const MobileScanner({
33 super.key, 30 super.key,
34 required this.onDetect, 31 required this.onDetect,
35 this.controller, 32 this.controller,
  33 + this.autoResume = true,
36 this.fit = BoxFit.cover, 34 this.fit = BoxFit.cover,
37 - this.allowDuplicates = false,  
38 this.onPermissionSet, 35 this.onPermissionSet,
39 }); 36 });
40 37
@@ -55,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner> @@ -55,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner>
55 if (!controller.isStarting) controller.start(); 52 if (!controller.isStarting) controller.start();
56 } 53 }
57 54
  55 + AppLifecycleState? _lastState;
  56 +
58 @override 57 @override
59 void didChangeAppLifecycleState(AppLifecycleState state) { 58 void didChangeAppLifecycleState(AppLifecycleState state) {
60 switch (state) { 59 switch (state) {
61 case AppLifecycleState.resumed: 60 case AppLifecycleState.resumed:
62 - if (!controller.isStarting && controller.autoResume) controller.start(); 61 + if (!controller.isStarting &&
  62 + widget.autoResume &&
  63 + _lastState != AppLifecycleState.inactive) controller.start();
63 break; 64 break;
64 - case AppLifecycleState.inactive:  
65 case AppLifecycleState.paused: 65 case AppLifecycleState.paused:
66 case AppLifecycleState.detached: 66 case AppLifecycleState.detached:
67 controller.stop(); 67 controller.stop();
68 break; 68 break;
  69 + default:
  70 + break;
69 } 71 }
  72 + _lastState = state;
70 } 73 }
71 74
72 - Uint8List? lastScanned;  
73 -  
74 @override 75 @override
75 Widget build(BuildContext context) { 76 Widget build(BuildContext context) {
76 return ValueListenableBuilder( 77 return ValueListenableBuilder(
77 - valueListenable: controller.args, 78 + valueListenable: controller.startArguments,
78 builder: (context, value, child) { 79 builder: (context, value, child) {
79 value = value as MobileScannerArguments?; 80 value = value as MobileScannerArguments?;
80 if (value == null) { 81 if (value == null) {
81 return const ColoredBox(color: Colors.black); 82 return const ColoredBox(color: Colors.black);
82 } else { 83 } else {
83 controller.barcodes.listen((barcode) { 84 controller.barcodes.listen((barcode) {
84 - if (!widget.allowDuplicates) {  
85 - if (lastScanned != barcode.rawBytes) {  
86 - lastScanned = barcode.rawBytes;  
87 - widget.onDetect(barcode, value! as MobileScannerArguments);  
88 - }  
89 - } else {  
90 - widget.onDetect(barcode, value! as MobileScannerArguments);  
91 - } 85 + widget.onDetect(barcode, value! as MobileScannerArguments);
92 }); 86 });
93 return ClipRect( 87 return ClipRect(
94 child: SizedBox( 88 child: SizedBox(
@@ -5,170 +5,155 @@ import 'package:flutter/cupertino.dart'; @@ -5,170 +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
  11 +/// The [MobileScannerController] holds all the logic of this plugin,
  12 +/// where as the [MobileScanner] class is the frontend of this plugin.
10 class MobileScannerController { 13 class MobileScannerController {
11 - MethodChannel methodChannel =  
12 - const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');  
13 - EventChannel eventChannel =  
14 - const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); 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 + }
15 33
16 //Must be static to keep the same value on new instances 34 //Must be static to keep the same value on new instances
17 - static int? _controllerHashcode;  
18 - StreamSubscription? events; 35 + static int? controllerHashcode;
19 36
20 - Function(bool permissionGranted)? onPermissionSet;  
21 - final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null);  
22 - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);  
23 - late final ValueNotifier<CameraFacing> cameraFacingState;  
24 - final Ratio? ratio;  
25 - final bool? torchEnabled;  
26 - // Whether to return the image buffer with the Barcode event 37 + /// Select which camera should be used.
  38 + ///
  39 + /// Default: CameraFacing.back
  40 + final CameraFacing facing;
  41 +
  42 + // /// Analyze the image in 4:3 or 16:9
  43 + // ///
  44 + // /// Only on Android
  45 + // final Ratio? ratio;
  46 +
  47 + /// Enable or disable the torch (Flash) on start
  48 + ///
  49 + /// Default: disabled
  50 + final bool torchEnabled;
  51 +
  52 + /// Set to true if you want to return the image buffer with the Barcode event
  53 + ///
  54 + /// Only supported on iOS and Android
27 final bool returnImage; 55 final bool returnImage;
28 56
29 - /// If provided, the scanner will only detect those specific formats. 57 + /// If provided, the scanner will only detect those specific formats
30 final List<BarcodeFormat>? formats; 58 final List<BarcodeFormat>? formats;
31 59
32 - CameraFacing facing;  
33 - bool hasTorch = false;  
34 - late StreamController<Barcode> barcodesController;  
35 -  
36 - /// Whether to automatically resume the camera when the application is resumed  
37 - bool autoResume; 60 + /// Sets the speed of detections.
  61 + ///
  62 + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
  63 + final DetectionSpeed detectionSpeed;
38 64
39 - Stream<Barcode> get barcodes => barcodesController.stream; 65 + /// Sets the barcode stream
  66 + final StreamController<BarcodeCapture> _barcodesController =
  67 + StreamController.broadcast();
  68 + Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
40 69
41 - MobileScannerController({  
42 - this.facing = CameraFacing.back,  
43 - this.ratio,  
44 - this.torchEnabled,  
45 - this.formats,  
46 - this.onPermissionSet,  
47 - this.autoResume = true,  
48 - this.returnImage = false,  
49 - }) {  
50 - // In case a new instance is created before calling dispose()  
51 - if (_controllerHashcode != null) {  
52 - stop();  
53 - }  
54 - _controllerHashcode = hashCode; 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');
55 74
56 - cameraFacingState = ValueNotifier(facing); 75 + Function(bool permissionGranted)? onPermissionSet;
57 76
58 - // Sets analyze mode and barcode stream  
59 - barcodesController = StreamController.broadcast(  
60 - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),  
61 - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),  
62 - ); 77 + /// Listen to events from the platform specific code
  78 + late StreamSubscription events;
63 79
64 - // Listen to events from the platform specific code  
65 - events = eventChannel  
66 - .receiveBroadcastStream()  
67 - .listen((data) => handleEvent(data as Map));  
68 - } 80 + /// A notifier that provides several arguments about the MobileScanner
  81 + final ValueNotifier<MobileScannerArguments?> startArguments =
  82 + ValueNotifier(null);
69 83
70 - void handleEvent(Map event) {  
71 - final name = event['name'];  
72 - final data = event['data'];  
73 - final binaryData = event['binaryData'];  
74 - switch (name) {  
75 - case 'torchState':  
76 - final state = TorchState.values[data as int? ?? 0];  
77 - torchState.value = state;  
78 - break;  
79 - case 'barcode':  
80 - final image = returnImage ? event['image'] as Uint8List : null;  
81 - final barcode = Barcode.fromNative(data as Map? ?? {}, image);  
82 - barcodesController.add(barcode);  
83 - break;  
84 - case 'barcodeMac':  
85 - barcodesController.add(  
86 - Barcode(  
87 - rawValue: (data as Map)['payload'] as String?,  
88 - ),  
89 - );  
90 - break;  
91 - case 'barcodeWeb':  
92 - final bytes = (binaryData as List).cast<int>();  
93 - barcodesController.add(  
94 - Barcode(  
95 - rawValue: data as String?,  
96 - rawBytes: Uint8List.fromList(bytes),  
97 - ),  
98 - );  
99 - break;  
100 - default:  
101 - throw UnimplementedError();  
102 - }  
103 - } 84 + /// A notifier that provides the state of the Torch (Flash)
  85 + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
104 86
105 - // TODO: Add more analyzers like text analyzer  
106 - // void setAnalyzeMode(int mode) {  
107 - // if (hashCode != _controllerHashcode) {  
108 - // return;  
109 - // }  
110 - // methodChannel.invokeMethod('analyze', mode);  
111 - // } 87 + /// A notifier that provides the state of which camera is being used
  88 + late final ValueNotifier<CameraFacing> cameraFacingState =
  89 + ValueNotifier(facing);
112 90
113 - // List<BarcodeFormats>? formats = _defaultBarcodeFormats,  
114 bool isStarting = false; 91 bool isStarting = false;
  92 + bool? _hasTorch;
  93 +
  94 + /// Set the starting arguments for the camera
  95 + Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
  96 + final Map<String, dynamic> arguments = {};
  97 +
  98 + cameraFacingState.value = cameraFacingOverride ?? facing;
  99 + arguments['facing'] = cameraFacingState.value.index;
  100 +
  101 + // if (ratio != null) arguments['ratio'] = ratio;
  102 + arguments['torch'] = torchEnabled;
  103 + arguments['speed'] = detectionSpeed.index;
  104 +
  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 + }
  111 + }
  112 + arguments['returnImage'] = true;
  113 + return arguments;
  114 + }
115 115
116 /// Start barcode scanning. This will first check if the required permissions 116 /// Start barcode scanning. This will first check if the required permissions
117 /// are set. 117 /// are set.
118 - Future<void> start() async {  
119 - ensure('startAsync'); 118 + Future<MobileScannerArguments?> start({
  119 + CameraFacing? cameraFacingOverride,
  120 + }) async {
  121 + debugPrint('Hashcode controller: $hashCode');
120 if (isStarting) { 122 if (isStarting) {
121 - throw Exception('mobile_scanner: Called start() while already starting.'); 123 + debugPrint("Called start() while starting.");
122 } 124 }
123 isStarting = true; 125 isStarting = true;
124 - // setAnalyzeMode(AnalyzeMode.barcode.index);  
125 126
126 // Check authorization status 127 // Check authorization status
127 if (!kIsWeb) { 128 if (!kIsWeb) {
128 - MobileScannerState state = MobileScannerState  
129 - .values[await methodChannel.invokeMethod('state') as int? ?? 0]; 129 + final MobileScannerState state = MobileScannerState
  130 + .values[await _methodChannel.invokeMethod('state') as int? ?? 0];
130 switch (state) { 131 switch (state) {
131 case MobileScannerState.undetermined: 132 case MobileScannerState.undetermined:
132 final bool result = 133 final bool result =
133 - await methodChannel.invokeMethod('request') as bool? ?? false;  
134 - state = result  
135 - ? MobileScannerState.authorized  
136 - : MobileScannerState.denied;  
137 - 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 + }
138 break; 140 break;
139 case MobileScannerState.denied: 141 case MobileScannerState.denied:
140 isStarting = false; 142 isStarting = false;
141 onPermissionSet?.call(false); 143 onPermissionSet?.call(false);
142 - throw PlatformException(code: 'NO ACCESS'); 144 + throw MobileScannerException('User declined camera permission.');
143 case MobileScannerState.authorized: 145 case MobileScannerState.authorized:
144 onPermissionSet?.call(true); 146 onPermissionSet?.call(true);
145 break; 147 break;
146 } 148 }
147 } 149 }
148 150
149 - cameraFacingState.value = facing;  
150 -  
151 - // Set the starting arguments for the camera  
152 - final Map arguments = {};  
153 - arguments['facing'] = facing.index;  
154 - if (ratio != null) arguments['ratio'] = ratio;  
155 - if (torchEnabled != null) arguments['torch'] = torchEnabled;  
156 -  
157 - if (formats != null) {  
158 - if (Platform.isAndroid) {  
159 - arguments['formats'] = formats!.map((e) => e.index).toList();  
160 - } else if (Platform.isIOS || Platform.isMacOS) {  
161 - arguments['formats'] = formats!.map((e) => e.rawValue).toList();  
162 - }  
163 - }  
164 - arguments['returnImage'] = returnImage;  
165 -  
166 // Start the camera with arguments 151 // Start the camera with arguments
167 Map<String, dynamic>? startResult = {}; 152 Map<String, dynamic>? startResult = {};
168 try { 153 try {
169 - startResult = await methodChannel.invokeMapMethod<String, dynamic>( 154 + startResult = await _methodChannel.invokeMapMethod<String, dynamic>(
170 'start', 155 'start',
171 - arguments, 156 + _argumentsToMap(cameraFacingOverride: cameraFacingOverride),
172 ); 157 );
173 } on PlatformException catch (error) { 158 } on PlatformException catch (error) {
174 debugPrint('${error.code}: ${error.message}'); 159 debugPrint('${error.code}: ${error.message}');
@@ -176,85 +161,78 @@ class MobileScannerController { @@ -176,85 +161,78 @@ class MobileScannerController {
176 if (error.code == "MobileScannerWeb") { 161 if (error.code == "MobileScannerWeb") {
177 onPermissionSet?.call(false); 162 onPermissionSet?.call(false);
178 } 163 }
179 - // setAnalyzeMode(AnalyzeMode.none.index);  
180 - return; 164 + return null;
181 } 165 }
182 166
183 if (startResult == null) { 167 if (startResult == null) {
184 isStarting = false; 168 isStarting = false;
185 - throw PlatformException(code: 'INITIALIZATION ERROR'); 169 + throw MobileScannerException(
  170 + 'Failed to start mobileScanner, no response from platform side',
  171 + );
186 } 172 }
187 173
188 - hasTorch = startResult['torchable'] as bool? ?? false; 174 + _hasTorch = startResult['torchable'] as bool? ?? false;
  175 + if (_hasTorch! && torchEnabled) {
  176 + torchState.value = TorchState.on;
  177 + }
189 178
190 if (kIsWeb) { 179 if (kIsWeb) {
191 onPermissionSet?.call( 180 onPermissionSet?.call(
192 true, 181 true,
193 ); // 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
194 183
195 - args.value = MobileScannerArguments( 184 + startArguments.value = MobileScannerArguments(
196 webId: startResult['ViewID'] as String?, 185 webId: startResult['ViewID'] as String?,
197 size: Size( 186 size: Size(
198 startResult['videoWidth'] as double? ?? 0, 187 startResult['videoWidth'] as double? ?? 0,
199 startResult['videoHeight'] as double? ?? 0, 188 startResult['videoHeight'] as double? ?? 0,
200 ), 189 ),
201 - hasTorch: hasTorch, 190 + hasTorch: _hasTorch!,
202 ); 191 );
203 } else { 192 } else {
204 - args.value = MobileScannerArguments( 193 + startArguments.value = MobileScannerArguments(
205 textureId: startResult['textureId'] as int?, 194 textureId: startResult['textureId'] as int?,
206 size: toSize(startResult['size'] as Map? ?? {}), 195 size: toSize(startResult['size'] as Map? ?? {}),
207 - hasTorch: hasTorch, 196 + hasTorch: _hasTorch!,
208 ); 197 );
209 } 198 }
210 -  
211 isStarting = false; 199 isStarting = false;
  200 + return startArguments.value!;
212 } 201 }
213 202
  203 + /// Stops the camera, but does not dispose this controller.
214 Future<void> stop() async { 204 Future<void> stop() async {
215 - try {  
216 - await methodChannel.invokeMethod('stop');  
217 - } on PlatformException catch (error) {  
218 - debugPrint('${error.code}: ${error.message}');  
219 - } 205 + await _methodChannel.invokeMethod('stop');
220 } 206 }
221 207
222 /// Switches the torch on or off. 208 /// Switches the torch on or off.
223 /// 209 ///
224 /// Only works if torch is available. 210 /// Only works if torch is available.
225 Future<void> toggleTorch() async { 211 Future<void> toggleTorch() async {
226 - ensure('toggleTorch');  
227 - if (!hasTorch) {  
228 - debugPrint('Device has no torch/flash.');  
229 - 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');
230 } 218 }
231 219
232 - final TorchState state = 220 + torchState.value =
233 torchState.value == TorchState.off ? TorchState.on : TorchState.off; 221 torchState.value == TorchState.off ? TorchState.on : TorchState.off;
234 222
235 - try {  
236 - await methodChannel.invokeMethod('torch', state.index);  
237 - } on PlatformException catch (error) {  
238 - debugPrint('${error.code}: ${error.message}');  
239 - } 223 + await _methodChannel.invokeMethod('torch', torchState.value.index);
240 } 224 }
241 225
242 /// Switches the torch on or off. 226 /// Switches the torch on or off.
243 /// 227 ///
244 /// Only works if torch is available. 228 /// Only works if torch is available.
245 Future<void> switchCamera() async { 229 Future<void> switchCamera() async {
246 - ensure('switchCamera');  
247 - try {  
248 - await methodChannel.invokeMethod('stop');  
249 - } on PlatformException catch (error) {  
250 - debugPrint(  
251 - '${error.code}: camera is stopped! Please start before switching camera.',  
252 - );  
253 - return;  
254 - }  
255 - facing =  
256 - facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back;  
257 - 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);
258 } 236 }
259 237
260 /// Handles a local image file. 238 /// Handles a local image file.
@@ -263,28 +241,72 @@ class MobileScannerController { @@ -263,28 +241,72 @@ class MobileScannerController {
263 /// 241 ///
264 /// [path] The path of the image on the devices 242 /// [path] The path of the image on the devices
265 Future<bool> analyzeImage(String path) async { 243 Future<bool> analyzeImage(String path) async {
266 - return methodChannel 244 + return _methodChannel
267 .invokeMethod<bool>('analyzeImage', path) 245 .invokeMethod<bool>('analyzeImage', path)
268 .then<bool>((bool? value) => value ?? false); 246 .then<bool>((bool? value) => value ?? false);
269 } 247 }
270 248
271 /// 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.
272 void dispose() { 252 void dispose() {
273 - if (hashCode == _controllerHashcode) {  
274 - stop();  
275 - events?.cancel();  
276 - events = null;  
277 - _controllerHashcode = null; 253 + stop();
  254 + events.cancel();
  255 + _barcodesController.close();
  256 + if (hashCode == controllerHashcode) {
  257 + controllerHashcode = null;
278 onPermissionSet = null; 258 onPermissionSet = null;
279 } 259 }
280 - barcodesController.close();  
281 } 260 }
282 261
283 - /// Checks if the MobileScannerController is bound to the correct MobileScanner object.  
284 - void ensure(String name) {  
285 - final message =  
286 - 'MobileScannerController.$name called after MobileScannerController.dispose\n'  
287 - 'MobileScannerController methods should not be used after calling dispose.';  
288 - 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 + }
289 } 311 }
290 } 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