feat: Add BlinkIdScannerView for custom scanner UI - #100
Conversation
Adds a Flutter PlatformView that exposes the BlinkID camera scanning engine with a fully custom Flutter overlay, alongside the existing performScan (built-in native UX) path. New public API: - BlinkIdScannerController (ChangeNotifier) — scan lifecycle, phase, guidance. Call initialize() then scan(); retry via reset(). - BlinkIdScannerView — platform view rendering the native camera surface (Android: CameraX TextureView COMPATIBLE mode; iOS: AVFoundation). - BlinkIdScanPhase — front / flip / back. Call onFlipComplete() after your flip animation or the scanner pauses indefinitely. - BlinkIdScannerStatus — uninitialized → loadingSdk → initializing → ready → scanning → processing → done | error | cameraPermissionRequired. - BlinkIdGuidance sealed class — per-frame hints (tooFar, tooClose, blur, glare, tilted, notFullyVisible, wrongSide, …). - PreferredCamera enum + preferredCamera param on initialize(). - BlinkIdScannerController.switchCamera() — runtime camera switch; reinitializes native session, cancels in-progress scan cleanly. - BlinkIdScannerController.setDebugLogging(bool) — opt-in native lifecycle forwarding to Flutter debugPrint; no channel traffic unless enabled. Implementation: - Android: ImageAnalysis → session.process() on single-thread executor; runBlocking bridges coroutine. TextureView COMPATIBLE mode composites into Flutter layer hierarchy. - iOS: AVCaptureVideoDataOutput → session.process() on @ProcessingActor; isProcessingFrame guard prevents 30 fps backlog. Orientation via UIDevice.orientationDidChangeNotification. - Flip gate: native pauses on SideScanned, resumes after resumeAfterFlip. - Camera permission: surfaced as cameraPermissionRequired status rather than a hard crash; retryAfterPermissionGrant() resumes. Fixes: - Android: SIGSEGV on camera switch — recreate BlinkID session instead of rebinding CameraX; 500 ms drain before unbindAll(). - Android: session identity guard drops stale callbacks from prior session. - iOS: NSNull vs absent-key distinction in ScanningSettings — null modules now correctly disable rather than falling back to SDK defaults. - iOS: logSessionSettings wrapped in #if DEBUG; source label now correctly identifies the calling path (performScan / directApi / customScanner). Example app: - custom_scanner_screen.dart — guidance overlay, flip animation, 10 s scan timeout with retry dialog, camera switch button. - home_screen.dart — structured result card: face avatar, personal info, document info, document images (front, back, signature). Docs: README updated with Custom scanner UI section covering lifecycle, guidance stream, flip phase, camera selection, and debug logging.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughChangesCustom scanner API and Flutter flow
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ExampleApp
participant BlinkIdScannerController
participant BlinkIdScannerView
participant NativeCamera
ExampleApp->>BlinkIdScannerController: initialize and scan
BlinkIdScannerController->>BlinkIdScannerView: create scanner platform view
BlinkIdScannerController->>NativeCamera: start native scanning
NativeCamera-->>BlinkIdScannerController: guidance and result events
BlinkIdScannerController-->>ExampleApp: update status, phase, and result
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
_controller.cancel() throws BlinkIdScanCancelException which is already caught in _startScanning() and calls _safePop(). The button's own _safePop() caused two Navigator.pop() calls.
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (3)
BlinkID/lib/src/scanner/blinkid_guidance.dart (2)
1-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConvert the header block to a doc comment.
BlinkIdGuidanceis exported as public API fromBlinkID/lib/blinkid_flutter.dart. The header uses//comments, so dartdoc does not attach this text to the class. Use///on the lines that describe the class contract, and keep internal implementation notes as//.♻️ Proposed doc-comment conversion
-// Guidance states emitted by [BlinkIdScannerController.guidanceStream]. -// -// Android-verified DetectionStatus mapping (from compiled SDK): -// CameraTooFar → tooFar +/// Guidance states emitted by [BlinkIdScannerController.guidanceStream]. +/// +/// Android-verified DetectionStatus mapping (from compiled SDK): +/// CameraTooFar → tooFar🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BlinkID/lib/src/scanner/blinkid_guidance.dart` around lines 1 - 17, Convert the public API description immediately above BlinkIdGuidance from // comments to /// doc-comment lines so dartdoc attaches the guidance-state mapping and stream behavior to the class; retain internal notes such as platform-specific implementation details as regular // comments.
41-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSurface unknown guidance values in debug builds.
fromStringmaps every unrecognized value tosearching. The header comment states that the iOS cases are unconfirmed. If the iOS native layer emits a different spelling, the overlay shows "searching" forever and no diagnostic appears. Add a debug-only signal so a native/Dart string mismatch is visible during development. Keep the safe production fallback.♻️ Proposed debug signal
static BlinkIdGuidance fromString(String value) => switch (value) { + 'searching' => const BlinkIdGuidance.searching(), 'tooFar' => const BlinkIdGuidance.tooFar(), @@ - _ => const BlinkIdGuidance.searching(), + _ => _unknown(value), }; + + static BlinkIdGuidance _unknown(String value) { + assert(() { + debugPrint('[BlinkID] Unknown guidance value: "$value"'); + return true; + }()); + return const BlinkIdGuidance.searching(); + }
_unknownrequiresimport 'package:flutter/foundation.dart';.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BlinkID/lib/src/scanner/blinkid_guidance.dart` around lines 41 - 55, Update BlinkIdGuidance.fromString to detect unrecognized values before the existing searching fallback, and emit the proposed _unknown debug signal using Flutter foundation utilities. Keep the production behavior unchanged by retaining searching as the fallback when assertions/debug-only code is disabled.BlinkID/lib/src/scanner/blinkid_scanner_controller.dart (1)
146-165: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCancel the previous guidance subscription before reassigning it.
Line 152 overwrites
_guidanceSubwithout cancelling the existing subscription. IfonPlatformViewCreatedruns a second time, the firstEventChannelsubscription stays active. Two listeners then drive_onGuidanceEvent, and the phase state machine advances twice per frame. A hot restart or a platform-view recreation reaches this path.Line 164 also sets
readyunconditionally, which would reset acameraPermissionRequiredorscanningstatus on recreation.🐛 Proposed fix
void onPlatformViewCreated(int id) { + _guidanceSub?.cancel().ignore(); + _guidanceSub = null; final methodChannel = MethodChannel('com.microblink.blinkid.flutter/scanner/$id');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BlinkID/lib/src/scanner/blinkid_scanner_controller.dart` around lines 146 - 165, Update onPlatformViewCreated to cancel or otherwise dispose the existing _guidanceSub before assigning the new EventChannel subscription, preventing duplicate guidance listeners after recreation. Also avoid unconditionally resetting the scanner status to ready; preserve cameraPermissionRequired or scanning states when the platform view is recreated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.kt`:
- Around line 389-403: Restore the ActivityResultListener registration in
onReattachedToActivityForConfigChanges alongside the existing permission
listener, matching the listener setup in onAttachedToActivity so performScan
delivers scan results after configuration changes.
In
`@BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.kt`:
- Around line 327-335: Suppress guidance events from obsolete scanning sessions
by validating session identity before dispatching normal guidance: in
BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.kt
lines 327-335, require scanningSession === session before
guidanceEventSink?.success; apply the equivalent blinkIdSession === session
check in
BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift
lines 310-318. Preserve the existing guidance selection logic.
- Around line 342-345: Wrap the camera selection, unbind, and bind operations in
the scanner view’s camera setup flow with try/catch, including
resolveCameraSelector and bindToLifecycle. When an exception occurs, invoke
methodChannel.invokeMethod("onScanError", ...) with the error details so
main-executor camera failures are reported instead of escaping unhandled.
- Around line 276-277: Wrap the imageProxy processing logic in a try/finally
block to guarantee imageProxy closure even when exceptions occur. Move the
current imageProxy.close() call from the success path into a finally block, and
ensure the try block includes the InputImage.createFromCameraXImageProxy and
session.process calls along with any post-processing result handling that
follows them. This ensures imageProxy is closed regardless of whether InputImage
creation, session.process execution, or downstream result handling throws an
exception.
In `@BlinkID/example/ios/Runner.xcodeproj/project.pbxproj`:
- Line 387: Remove the hardcoded DEVELOPMENT_TEAM assignment from all listed
build configuration entries in the project, leaving team selection available to
each developer’s local Xcode signing settings.
In `@BlinkID/example/lib/custom_scanner_screen.dart`:
- Around line 209-250: Update the permission button handler in the scanner
screen to request Permission.camera before invoking
_controller.retryAfterPermissionGrant(). Only reset and restart scanning after
the OS reports that camera permission was granted, while preserving the existing
permanentlyDenied behavior and _startScanning flow.
- Around line 104-107: Update _switchCamera to serialize camera changes: track
whether a switch is in progress, and queue the latest requested camera or reject
additional requests until _controller.switchCamera settles. Ensure the
in-progress state is cleared on both success and failure, preventing concurrent
native switchCamera calls while preserving the requested camera state.
- Around line 20-31: Make _safePop idempotent so repeated calls from the close
handler and scanner loop exit the scanner route only once. Add a navigation
guard that is set before popping, and have subsequent calls return without
invoking Navigator.pop while preserving timer cancellation and mounted checks.
In `@BlinkID/example/lib/home_screen.dart`:
- Around line 62-69: Update _openCustomScanner to clear both _result and _error
before pushing CustomScannerScreen, matching the reset behavior in _performScan.
Preserve the existing result assignment after navigation returns, including
leaving the cleared state intact when the custom scan is canceled.
In `@BlinkID/example/README.md`:
- Around line 15-19: The README documents DirectAPI scanning from device gallery
images with MultiSide and SingleSide options, but home_screen.dart only exposes
native and custom scan actions, and pubspec.yaml has no gallery integration
dependencies, making the documented flow unrunnable. Either remove the DirectAPI
scanning section from the README entirely, or implement the missing gallery
integration by adding required dependencies to pubspec.yaml and adding the
DirectAPI scan actions to home_screen.dart to match the documented behavior.
- Line 34: Add language specifiers to the fenced code blocks in the README.md
file to comply with markdown linting. At line 34 where the environment key
example is shown, add the language identifier dotenv after the opening backticks
(```dotenv). At line 61 where the directory tree is shown, add the language
identifier text after the opening backticks (```text). This will resolve the
MD040 linting violations for both code blocks.
- Around line 33-43: The README instructions tell users to set license keys in
.env and then run plain flutter run, but home_screen.dart uses
String.fromEnvironment() to read BLINKID_LICENSE_KEY_ANDROID and
BLINKID_LICENSE_KEY_IOS as compile-time Dart defines, which plain flutter run
does not provide. Update the "Run" section in the README to document the correct
command syntax using --dart-define flags to pass the license keys at compile
time, or alternatively add a .env loader implementation to the app so the plain
flutter run approach works as documented.
In
`@BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift`:
- Around line 147-157: Update startScan’s asynchronous Task around
createScanningSession to invalidate work during scanner teardown using a
disposal or generation token. Capture the token before awaiting session
creation, then validate it after the await and return without assigning
blinkIdSession, setting isScanning, or calling result if teardown occurred;
apply the same guard to the corresponding flow at the additional referenced
location.
- Around line 28-33: The properties isScanning, isProcessingResult,
isProcessingFrame, currentFrameOrientation, debugLoggingEnabled, and
blinkIdSession are marked with nonisolated(unsafe) which bypasses Swift's
actor-based synchronization, but they are accessed and mutated from multiple
concurrent execution contexts including MainActor, capture-output queue,
ProcessingActor, and callbacks. Remove the nonisolated(unsafe) modifier from
these properties and implement proper synchronization by either placing them
behind a dedicated actor or lock-based serializer to guarantee safe access
across all these concurrent contexts.
In `@BlinkID/lib/src/scanner/blinkid_scanner_controller.dart`:
- Around line 474-484: Update
BlinkID/lib/src/scanner/blinkid_scanner_controller.dart at lines 474-484 and
418-444: add a _disposed flag and shared pending-completer list; in dispose(),
set _disposed first, unregister the method-call handler before invoking native
disposal, and complete every pending _awaitReady completer with
BlinkIdScanDisposeException. In _handleMethodCall and _setStatus, return
immediately when disposed; in the scan/_awaitReady flow, return Future.error
with the same exception when disposed and register each local completer for
disposal draining.
- Around line 418-444: Update _awaitReady and dispose so readiness waiters are
tracked in a _pendingReady collection and every pending completer is completed
with BlinkIdScanDisposeException during disposal. Ensure listener registration
and removal are skipped or safely handled once disposed, so callers awaiting
scan() do not hang or trigger ChangeNotifier errors.
- Around line 126-141: Update initialize so a failed loadBlinkIdSdk attempt
restores _status to BlinkIdScannerStatus.uninitialized before rethrowing,
allowing subsequent initialization retries and preventing reset from exposing
incomplete creation parameters. Preserve the existing wrapped exception behavior
and success path.
In `@README.md`:
- Around line 675-677: Update the code fence introducing the BlinkID.strings
example in the README to declare the strings language as `strings`, while
leaving the example content unchanged.
---
Nitpick comments:
In `@BlinkID/lib/src/scanner/blinkid_guidance.dart`:
- Around line 1-17: Convert the public API description immediately above
BlinkIdGuidance from // comments to /// doc-comment lines so dartdoc attaches
the guidance-state mapping and stream behavior to the class; retain internal
notes such as platform-specific implementation details as regular // comments.
- Around line 41-55: Update BlinkIdGuidance.fromString to detect unrecognized
values before the existing searching fallback, and emit the proposed _unknown
debug signal using Flutter foundation utilities. Keep the production behavior
unchanged by retaining searching as the fallback when assertions/debug-only code
is disabled.
In `@BlinkID/lib/src/scanner/blinkid_scanner_controller.dart`:
- Around line 146-165: Update onPlatformViewCreated to cancel or otherwise
dispose the existing _guidanceSub before assigning the new EventChannel
subscription, preventing duplicate guidance listeners after recreation. Also
avoid unconditionally resetting the scanner status to ready; preserve
cameraPermissionRequired or scanning states when the platform view is recreated.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 85dd3a58-28e5-4a46-ac33-a068ac8362a4
⛔ Files ignored due to path filters (25)
BlinkID/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.pngis excluded by!**/*.pngBlinkID/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.pngis excluded by!**/*.pngBlinkID/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.pngis excluded by!**/*.pngBlinkID/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngis excluded by!**/*.pngBlinkID/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngis excluded by!**/*.pngBlinkID/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedatais excluded by!**/*.xcworkspace/contents.xcworkspacedataBlinkID/example/ios/Runner.xcworkspace/contents.xcworkspacedatais excluded by!**/*.xcworkspace/contents.xcworkspacedataBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.pngis excluded by!**/*.pngBlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.pngis excluded by!**/*.png
📒 Files selected for processing (68)
.vscode/launch.jsonBlinkID/.gitignoreBlinkID/android/build.gradleBlinkID/android/settings.gradleBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.ktBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerViewFactory.ktBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidDeserializationUtils.ktBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.ktBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidSerializationUtils.ktBlinkID/build.yamlBlinkID/example/.env.exampleBlinkID/example/.gitignoreBlinkID/example/.metadataBlinkID/example/README.mdBlinkID/example/analysis_options.yamlBlinkID/example/android/app/build.gradleBlinkID/example/android/app/src/main/AndroidManifest.xmlBlinkID/example/android/app/src/main/kotlin/com/flutter/scanner/example/MainActivity.ktBlinkID/example/android/app/src/main/res/drawable/ic_launcher_background.xmlBlinkID/example/android/app/src/main/res/drawable/ic_launcher_foreground.xmlBlinkID/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xmlBlinkID/example/android/app/src/main/res/values/styles.xmlBlinkID/example/android/build.gradleBlinkID/example/android/gradle.propertiesBlinkID/example/android/gradle/gradle-daemon-jvm.propertiesBlinkID/example/android/gradle/wrapper/gradle-wrapper.propertiesBlinkID/example/android/settings.gradleBlinkID/example/ios/.gitignoreBlinkID/example/ios/Flutter/AppFrameworkInfo.plistBlinkID/example/ios/Flutter/Debug.xcconfigBlinkID/example/ios/Flutter/Release.xcconfigBlinkID/example/ios/Runner.xcodeproj/project.pbxprojBlinkID/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistBlinkID/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettingsBlinkID/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcschemeBlinkID/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistBlinkID/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettingsBlinkID/example/ios/Runner/AppDelegate.swiftBlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.jsonBlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.jsonBlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.mdBlinkID/example/ios/Runner/Base.lproj/LaunchScreen.storyboardBlinkID/example/ios/Runner/Base.lproj/Main.storyboardBlinkID/example/ios/Runner/Info.plistBlinkID/example/ios/Runner/Runner-Bridging-Header.hBlinkID/example/ios/Runner/SceneDelegate.swiftBlinkID/example/ios/RunnerTests/RunnerTests.swiftBlinkID/example/lib/app.dartBlinkID/example/lib/custom_scanner_screen.dartBlinkID/example/lib/home_screen.dartBlinkID/example/lib/main.dartBlinkID/example/pubspec.yamlBlinkID/example/test/widget_test.dartBlinkID/ios/blinkid_flutter/Package.swiftBlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swiftBlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerViewFactory.swiftBlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkidFlutterPlugin.swiftBlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/Serialization/BlinkIdDeserializationUtils.swiftBlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/Serialization/BlinkIdSerializationUtils.swiftBlinkID/lib/blinkid_flutter.dartBlinkID/lib/src/blinkid_settings.dartBlinkID/lib/src/blinkid_settings.g.dartBlinkID/lib/src/scanner/blinkid_guidance.dartBlinkID/lib/src/scanner/blinkid_scanner_controller.dartBlinkID/lib/src/scanner/blinkid_scanner_view.dartBlinkID/lib/src/types.dartBlinkID/lib/src/types.g.dartREADME.md
Replace the all-null early-return block with inline ?: defaults per field. Semantics unchanged — absent/null key still yields SDK-default module settings — but mirrors how iOS handles the same case per-field.
iOS BlinkIdScannerView: - Replace nonisolated(unsafe) vars with NSLock for correct cross-thread access - Add _claimFrame() atomic helper to eliminate separate guard + assignment races - Track _startScanTask so in-flight session setup can be cancelled on teardown - Guard guidanceEventSink callbacks against stale session references Android BlinkIdScannerView: - Wrap frame analysis callback in try-catch to prevent uncaught exceptions crashing the analyzer thread - Add addActivityResultListener in onAttachedToActivity Dart scanner controller: - On SDK load failure reset to uninitialized instead of error state (caller decides) - Cancel existing guidance subscription in onPlatformViewCreated to prevent leaks on reuse - Track _pendingReady completers and reject them all on dispose Dart blinkid_guidance: - Promote block comments to doc comments; add _unknown() fallback for unmapped strings Example app: - Add permission_handler for proper system settings redirect on permanently denied camera - Guard _safePop with _isPopping flag to prevent concurrent double-pop race - Guard _switchCamera with _isSwitchingCamera flag - Clear result/error state before opening custom scanner - Remove DEVELOPMENT_TEAM from example pbxproj; update README
Android emits the string "searching" for the searching/idle state but it had no entry in the from-string switch, falling through to _unknown().
Dart: - Add BlinkIdScanCameraSwitchException distinct from BlinkIdScanCancelException so scan loop can continue after switchCamera() without treating it as cancel - Guard scan() against double-start with StateError - Gate _handleMethodCall error logging on _debugLoggingEnabled - Update guidance comment to reflect neither platform emits holdStill/blur/glare Android BlinkIdScannerView: - Mark debugLoggingEnabled/scanningSession/isScanning/preferredCameraOverride @volatile — all read from analyzer thread, written from main thread - Drain analysisExecutor before setupCamera() in retryCamera and switchCamera to prevent race between in-flight frame and CameraX rebind - Guard startScan against concurrent call (pendingStartResult != null) - Catch and log analyzer exceptions via onDebugLog instead of crashing silently - Move lifecycleRegistry.DESTROYED after executor drain in teardown() iOS BlinkIdScannerView: - Emit onScanError when camera device or session setup fails instead of returning silently, so Flutter can surface the error state Example: - Catch BlinkIdScanCameraSwitchException separately with continue - Remove the status == .initializing hack from BlinkIdScanCancelException handler
Android & iOS BlinkIdScannerView: - Emit 'blur' / 'glare' guidance from frame analysis (blurDetectionStatus / glareDetectionStatus == Detected) ahead of the DetectionStatus fallback - Pass scan result as raw JSON string to onScanResult instead of deserializing to a nested Map — avoids Map<Object?,Object?> from StandardMessageCodec and removes the toNestedMap/toNestedList helpers - Fix EventChannel teardown ordering: null guidanceEventSink in teardown() for immediate effect; setStreamHandler(null/nil) in the 'dispose' method-channel handler (Dart's 'cancel' always arrives first on same queue) Dart scanner controller: - cancel() when status is initializing now rejects all _pendingReady completers with BlinkIdScanCancelException so scan() unblocks immediately (fixes X button being stuck during camera switch) - Simplify onScanResult JSON parsing: String path skips redundant Map.from() Dart blinkid_guidance: - Promote blur/glare to "Quality hints (emitted by both platforms)" section - Update doc comment mapping table to reflect confirmed platform behaviour
Android: barcodeModule was missing ?: BarcodeModuleSettings() fallback added in the earlier ScanningSettings refactor — now consistent with mrz/viz/documentCapture modules. Example: BlinkIdSdkInitException during initialize() now shows a snackbar and pops the scanner instead of returning silently. README: correct switchCamera() docs to reference BlinkIdScanCameraSwitchException (was stale BlinkIdScanCancelException after the dedicated exception was added).
…t-cam rotation Camera result deferral (Android + iOS): - switchCamera/retryCamera no longer complete their Flutter result immediately; result is held in pendingCameraResult and completed from completeCameraResult() once the camera has actually bound (or definitively failed). Dart await on switchCamera() is now accurate rather than resolving before the bind. - switchCamera() returns the lens actually bound (PreferredCamera), which may differ from the request on devices without a front camera. Also exposed as controller.activeCamera. - abortPendingStart() / abortPendingCameraResult() helpers clean up in-flight operations on cancel, switch, and dispose. Android: - startJob + ensureActive() guard: a session created during an in-flight startScan is discarded if cancelled before it can be installed. - resolveCameraLens() returns a String; preferredCameraOverride is updated to the actually resolved lens; cameraSelectorFor() is a separate helper. - resolveModuleSettings() generic replaces 4 nearly-identical optional*Settings() helpers in BlinkidDeserializationUtils. - All Log.i() → Log.d() gated behind Log.isLoggable(TAG, Log.DEBUG). - Plugin logs gated behind Log.isLoggable as well. - CAMERA permission declared in plugin manifest (merged automatically). iOS: - Front camera landscape rotation fix: front sensor is 180° rotated vs back in landscape; frontCameraLandscapeRotationAngle / frontCameraLandscapeAvOrientation extensions handle the inversion; automaticallyAdjustsVideoMirroring disabled before updateVideoOrientation() so manual mirroring isn't overridden. - preferredCameraOverride pinned to resolved lens early in performCameraSetup(). - UIApplication.shared.windows → connectedScenes (deprecated API fix). - Plugin logs wrapped in #if DEBUG. Dart scanner controller: - _disposed flag: guards _handleMethodCall, _onGuidanceEvent, _setStatus and notifyListeners() against late native callbacks racing dispose(). - dispose() sets _disposed first, nulls method call handler before invoking native dispose, closes guidance controller after. - switchCamera() returns Future<PreferredCamera>; _retryCamera() updates _activeCamera from the resolved lens string. - _lastEmittedGuidance: collapses consecutive duplicate guidance frames before they reach guidanceStream (native emits ~30/s; exact duplicates add no info). Phase change resets the last value so "searching" is re-emitted on back side. Dart BlinkIdScanningSettings: - Tri-state sentinel (_unsetModule): omit = SDK default (enabled), explicit null = disabled. @JsonSerializable(createFactory: false) + hand-written fromJson preserve absent-vs-null distinction on the way back in. - @jsonkey(includeIfNull: false) annotations removed; toJson() now emits null for disabled modules so native can honour the explicit disable. Example custom_scanner_screen.dart: - Guidance state lifted to parent State: single _guidanceSub drives both the scan-timeout timer and the display. _GuidanceOverlay takes text + switcherKey instead of a controller reference; _FlipOverlay takes an onFlipComplete callback. Phase change resets guidance text immediately. - _FlipOverlayState.dispose() calls _completeFlip() so onFlipComplete is guaranteed even if the overlay is torn down before animation finishes. - canInteract excludes status == .initializing to hide X/camera buttons during camera rebind. README: camera permission section, module omit-vs-null clarification, switchCamera return value, status table updated.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
README.md (2)
414-424: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHandle camera-switch cancellation in the scan loop.
switchCamera()is documented to complete the activescan()future withBlinkIdScanCameraSwitchException, but this loop catches onlyBlinkIdScanCancelExceptionandBlinkIdScanResetException. Add a camera-switch catch so_run()starts the next scan instead of ending with an unhandled exception.Proposed fix
} on BlinkIdScanResetException { // controller.reset() was called — loop restarts scan() + } on BlinkIdScanCameraSwitchException { + // switchCamera() canceled this scan; the loop restarts it. }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 414 - 424, Update the scan loop around _controller.scan() in _run() to catch BlinkIdScanCameraSwitchException and continue looping so the next scan starts, matching the existing reset handling; preserve the current cancel behavior that pops the navigator and returns.
393-405: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winImport
dart:asyncbefore callingunawaited.The quick-start imports only
package:blinkid_flutter/blinkid_flutter.dartbut callsunawaited(_run()); that helper comes fromdart:async, so this example will not compile unless the standard library is imported.Proposed fix
+import 'dart:async'; import 'package:blinkid_flutter/blinkid_flutter.dart';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 393 - 405, Add the dart:async import to the README quick-start example before using unawaited in _ScanScreenState.initState, while retaining the existing blinkid_flutter import and scanner setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift`:
- Line 331: Serialize all AVCaptureSession lifecycle and configuration
operations used by startScan(), switchCamera, and teardown() on a single serial
capture-session queue instead of the global queue. Ensure queued startRunning
work verifies that its captured session is still the current captureSession
before starting, so replaced or nulled sessions cannot restart or receive
frames.
---
Outside diff comments:
In `@README.md`:
- Around line 414-424: Update the scan loop around _controller.scan() in _run()
to catch BlinkIdScanCameraSwitchException and continue looping so the next scan
starts, matching the existing reset handling; preserve the current cancel
behavior that pops the navigator and returns.
- Around line 393-405: Add the dart:async import to the README quick-start
example before using unawaited in _ScanScreenState.initState, while retaining
the existing blinkid_flutter import and scanner setup.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: bb719ccc-4648-4372-8d0f-16b7a59e4f12
📒 Files selected for processing (11)
BlinkID/android/src/main/AndroidManifest.xmlBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.ktBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidDeserializationUtils.ktBlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.ktBlinkID/example/lib/custom_scanner_screen.dartBlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swiftBlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkidFlutterPlugin.swiftBlinkID/lib/src/blinkid_settings.dartBlinkID/lib/src/blinkid_settings.g.dartBlinkID/lib/src/scanner/blinkid_scanner_controller.dartREADME.md
🚧 Files skipped from review as they are similar to previous changes (6)
- BlinkID/lib/src/blinkid_settings.g.dart
- BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidDeserializationUtils.kt
- BlinkID/example/lib/custom_scanner_screen.dart
- BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkidFlutterPlugin.swift
- BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.kt
- BlinkID/lib/src/scanner/blinkid_scanner_controller.dart
…EADME fixes Use a per-view serial DispatchQueue for all startRunning()/stopRunning() calls: - Apple documents both as blocking — neither belongs on the main thread. - Serial ordering (enqueued from main thread in call order) prevents a stale session's queued start from running after a later switchCamera()/teardown() has already stopped and detached it — no generation counter needed. - stopCaptureSession() and teardown() capture the session reference before nil-ing captureSession, then dispatch stopRunning() on the queue so the stop is always ordered after this session's own queued start. README: add missing 'dart:async' import to custom scanner example; add BlinkIdScanCameraSwitchException catch to the scan loop snippet.
types.dart — BarcodeModuleSettings, DocumentCaptureModuleSettings, MrzModuleSettings, VizModuleSettings: - All fields marked final; constructors made const. - copyWith() added to each so callers can produce modified copies without constructing from scratch. blinkid_settings.dart — BlinkIdScanningSettings: - Replace _unsetModule sentinel + initialiser list with plain const-default parameters (this.field = const ModuleSettings()). Because the defaults are now const instances, the hand-written fromJson still distinguishes "key absent → leave the const default" from "key present and null → disabled" correctly via containsKey(). - Simplify the comment: the reason createFactory: false is still needed hasn't changed, only the constructor shape. sample_files/module_settings_panel.dart: - Switch all _updateBarcode/_update callbacks from direct mutation to copyWith() to match the now-immutable module settings types.
…t tests iOS: teardown() now calls stopCaptureSession() instead of repeating the same capture/nil/sessionQueue.async pattern inline — eliminates the risk of the two diverging and ensures videoOutput is also cleared on teardown. Tests (BlinkID/test/): - blinkid_scanner_controller_test.dart: pure-Dart state-machine tests driven via mocked method/event channels (no device or license required). Covers: in-flight startScan surviving cancel/switchCamera; switchCamera not reporting ready until the native reply actually lands and returning the resolved lens; late native callbacks after dispose() not throwing or corrupting status; in-flight scan completing with BlinkIdScanDisposeException. - blinkid_settings_test.dart: verifies the tri-state module contract on BlinkIdScanningSettings (unset → SDK-default enabled; explicit null → disabled; round-trip through toJson/fromJson is stable).
Summary
Introduces a custom platform view scanner. We were using a similar version with v6 on this branch at Mews.
The solution offered by the package is not up to the par with the capabilities of native SDKs Microblink offers. This PR aims to contribute back and offer a custom scanner experience, that can be customised by client.
ScreenRecording_08-04-2026.15-30-27_1.mp4
(Using debug mode on iPad over wireless, the app looks slow but works bearer in prod build)
This PR is intentionally large — it cannot be split smaller without shipping a broken intermediate state.
What's in here
BlinkIdScannerView— custom camera UIPlatformViewthat exposes the raw camera surfaceBlinkIdScannerController(ChangeNotifier) driving status, phase, and guidanceuninitialized → loadingSdk → initializing → ready → scanning → processing → done / errorBlinkIdScanPhase: front / flip / back — with native pause during flip and suppressed guidance eventsguidanceStream— per-frame detection hints (tooFar, blur, glare, etc.)preferredCameraoninitialize()+ runtimeswitchCamera()(reinitializes session; cancels in-progress scan cleanly)setDebugLogging(true)— opt-in native lifecycle forwarding to Flutter'sdebugPrintcameraPermissionRequiredstatusExample app
Docs
Summary by CodeRabbit