Skip to content

Refactor(android): replace zxing-android-embedded with CameraX + zxing-cpp - #28

Open
mahmourad98 wants to merge 3 commits into
vespr-wallet:masterfrom
mahmourad98:feat/camerax-zxing-cpp-android
Open

Refactor(android): replace zxing-android-embedded with CameraX + zxing-cpp#28
mahmourad98 wants to merge 3 commits into
vespr-wallet:masterfrom
mahmourad98:feat/camerax-zxing-cpp-android

Conversation

@mahmourad98

@mahmourad98 mahmourad98 commented Jul 20, 2026

Copy link
Copy Markdown

Summary

This PR replaces the Android scanner backend from zxing-android-embedded to a CameraX + zxing-cpp pipeline while keeping the Flutter-side MethodChannel contract and public Dart API unchanged.

The migration removes the deprecated Camera1-based dependency, moves camera preview/control to CameraX, and moves frame decoding to zxing-cpp. It also preserves overlay-restricted scanning by translating the Dart overlay cutout into analysis-buffer coordinates and applying that region through ImageProxy.cropRect.

Key Changes

Android build and dependency updates

  • Raises Android minSdkVersion from 20 to 21.
  • Removes com.journeyapps:zxing-android-embedded:4.3.0.
  • Removes com.google.zxing:core:3.5.4.
  • Adds CameraX 1.5.2 modules:
    • androidx.camera:camera-core
    • androidx.camera:camera-camera2
    • androidx.camera:camera-lifecycle
    • androidx.camera:camera-view
  • Adds io.github.zxing-cpp:android:3.1.0 as the decoder.

Android manifest updates

  • Declares android.permission.CAMERA directly in the plugin manifest.
  • Declares camera and autofocus hardware features as optional so apps remain installable on devices without a camera.
  • Removes the old tools:overrideLibrary usage tied to the previous ZXing Android dependency.

Scanner implementation changes

  • Replaces the old embedded ZXing camera view implementation with a PreviewView-based CameraX pipeline.
  • Uses PreviewView.ImplementationMode.COMPATIBLE so the preview renders through TextureView, which keeps the Flutter overlay visible above the camera preview.
  • Uses CameraX ImageAnalysis with a single-threaded executor to decode frames one at a time and drop stale frames.
  • Replaces the old custom framing view logic with CameraX transform-based region-of-interest mapping.
  • Applies the mapped scan area through ImageProxy.cropRect, which zxing-cpp decodes directly.
  • Drives pause/resume through a local LifecycleRegistry so CameraX owns session teardown and restart.
  • Preserves queued torch behavior across camera startup and rebinding.

Behavior fixes and intentional deltas

  • invertScan now responds on the MethodChannel instead of leaving the Dart future pending.
  • RSS-14 now reports RSS14, which matches the Dart parser expectation.
  • rawBytes now contains decoded content bytes from zxing-cpp, not ZXing raw codewords.
  • When Dart requests an empty format list, the Android side stays limited to the classic formats already representable by the Dart enum.

Files Changed

  • android/build.gradle
  • android/src/main/AndroidManifest.xml
  • android/src/main/kotlin/net/touchcapture/qr/flutterqrplus/QRView.kt
  • Deleted: android/src/main/kotlin/net/touchcapture/qr/flutterqrplus/CustomFramingRectBarcodeView.kt

Compatibility Notes

  • Public Dart API is unchanged.
  • MethodChannel name, method names, argument shapes, result types, and onRecognizeQR payload shape remain unchanged.
  • Android minimum SDK is now 21.

Summary by CodeRabbit

  • New Features
    • Android QR scanning now uses a modern CameraX-based camera stack with improved frame analysis and barcode format support.
    • Camera, flash, lens switching, scan-area updates, and permission handling continue to work through the existing Flutter interface.
    • Camera features are optional, allowing installation on devices without cameras.
  • Bug Fixes
    • Improved scanning lifecycle behavior when apps are backgrounded or permissions are pending.
    • Fixed scan inversion calls that could remain unresponsive.
  • Compatibility
    • Android minimum SDK is now 21.
    • Declared support for Android, iOS, and web platforms.

mahmourad98 and others added 3 commits July 17, 2026 14:45
…g-cpp

zxing-android-embedded has had no upstream release since 2021 and is built
on the deprecated Camera1 API. It provided both the camera layer and the
decoder, so replacing it means replacing both:

- Camera: androidx.camera 1.5.2 (Camera2-based, Google-maintained).
  PreviewView runs in COMPATIBLE (TextureView) mode -- the PERFORMANCE
  default renders through a SurfaceView, which SurfaceFlinger composites
  above Flutter's layer and hides QrScannerOverlayShape.
- Decoder: io.github.zxing-cpp:android 3.1.0, an actively maintained C++
  port of ZXing that decodes CameraX ImageProxy frames directly and honors
  ImageProxy.cropRect. Replaces com.google.zxing:core.

Scan-area restriction (previously CustomFramingRectBarcodeView, now deleted)
is implemented by mapping the Dart overlay's cutout from view coordinates
into analysis-buffer coordinates via PreviewView.sensorToViewTransform and
ImageInfo.sensorToBufferTransformMatrix, then setting ImageProxy.cropRect.

The MethodChannel contract with Dart is unchanged: same channel name, method
names, argument shapes, result types and onRecognizeQR payload. Public Dart
API is untouched. minSdk 20 -> 21 (CameraX/zxing-cpp floor; below Flutter's
own minimum anyway). The plugin now declares CAMERA itself, which apps
previously inherited via zxing-android-embedded's manifest merge.

Intentional behavior deltas, documented in code:
- rawBytes carries zxing-cpp's decoded content bytes; zxing's raw codeword
  stream has no equivalent API.
- RSS-14 now reports "RSS14", the string the Dart parser expects; the old
  code emitted "RSS_14", which made the Dart handler throw.
- invertScan now replies on the channel instead of leaving the future
  pending forever.

Verified: example app builds and scans on device, restricted to the overlay
cutout. Full on-device matrix (rotation, lens flip, torch, pause/resume)
still pending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Android QR scanning now uses CameraX and zxing-cpp, with updated lifecycle, permissions, ROI analysis, camera controls, and barcode mappings. Dart platform integration and package metadata were reformatted or updated for platform declarations and Flutter constraints.

Changes

Android scanning migration

Layer / File(s) Summary
Android camera dependencies and declarations
android/build.gradle, android/src/main/AndroidManifest.xml
The minimum SDK is 21, CameraX and zxing-cpp replace legacy ZXing dependencies, and camera permissions/features are declared.
CameraX preview and lifecycle pipeline
android/src/main/kotlin/net/touchcapture/qr/flutterqrplus/QRView.kt
QRView manages CameraX preview and analysis use cases, lifecycle state, ROI cropping, and decoder execution.
Camera controls, permissions, and barcode contracts
android/src/main/kotlin/net/touchcapture/qr/flutterqrplus/QRView.kt
Camera switching, torch state, scan control, permission callbacks, and zxing-cpp-to-Dart format mappings were updated.

Dart platform updates

Layer / File(s) Summary
Dart view and controller integration
lib/src/qr_code_scanner.dart, lib/src/platform/*, lib/src/types/*, lib/src/lifecycle_event_handler.dart
Platform-view creation, controller channel calls, platform information, enum formatting, and lifecycle declarations were updated or reformatted.
Cross-platform UI and web formatting
example/lib/main.dart, lib/src/qr_scanner_overlay_shape.dart, lib/src/web/*
Example, overlay, web, and stub code was reformatted without changing behavior.
Package platform metadata
pubspec.yaml
Android, iOS, and web platform metadata was added, and the Flutter constraint changed to ^3.44.0.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Flutter
  participant QRView
  participant CameraX
  participant zxing_cpp
  Flutter->>QRView: startScan(format indices)
  QRView->>CameraX: bind preview and analysis
  CameraX->>QRView: deliver image frame
  QRView->>zxing_cpp: decode cropped ROI
  zxing_cpp-->>QRView: return barcode result
  QRView-->>Flutter: invoke recognition callback
Loading

Suggested reviewers: alexdochioiu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main Android scanner backend migration from zxing-android-embedded to CameraX plus zxing-cpp.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mahmourad98 mahmourad98 changed the title Feat/camerax zxing cpp android Refactor(android): replace zxing-android-embedded with CameraX + zxing-cpp Jul 20, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates the Android implementation of the QR code scanner plugin from the deprecated zxing-android-embedded library to Google's modern CameraX API combined with zxing-cpp for barcode decoding. Key changes include the removal of CustomFramingRectBarcodeView and the introduction of a new QRView class that manages the camera lifecycle, streams frames via ImageAnalysis, and handles scan-area restrictions. The review feedback highlights several critical robustness improvements for the new Android code: wrapping future.get() and provider.bindToLifecycle in try-catch blocks to prevent crashes during camera initialization and binding, calling result.success(null) in startScan to prevent the Dart-side future from hanging, and catching Throwable instead of Exception during native barcode reading to handle potential linkage errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +313 to +318
val future = ProcessCameraProvider.getInstance(context)
future.addListener({
if (isDisposed) return@addListener
cameraProvider = future.get()
bindCameraUseCases()
}, ContextCompat.getMainExecutor(context))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling future.get() directly inside the listener without a try-catch block can lead to unhandled exceptions and crashes (e.g., ExecutionException or InterruptedException) if CameraX fails to initialize on certain devices or emulator environments. Wrapping it in a try-catch block ensures the app fails gracefully.

Suggested change
val future = ProcessCameraProvider.getInstance(context)
future.addListener({
if (isDisposed) return@addListener
cameraProvider = future.get()
bindCameraUseCases()
}, ContextCompat.getMainExecutor(context))
val future = ProcessCameraProvider.getInstance(context)
future.addListener({
if (isDisposed) return@addListener
try {
cameraProvider = future.get()
bindCameraUseCases()
} catch (e: Exception) {
// Handle or log CameraX initialization failure gracefully
}
}, ContextCompat.getMainExecutor(context))

Comment on lines +361 to +371
// RESUMED before binding so CameraX starts the session immediately.
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
val cam = provider.bindToLifecycle(this, selector, preview, analysis)
camera = cam
boundUseCases = listOf(preview, analysis)

// Re-apply a torch request made before/while the camera was down
// (old zxing setTorch() had the same queued-until-open behavior).
if (desiredTorchOn) {
cam.cameraControl.enableTorch(true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

provider.bindToLifecycle can throw an IllegalArgumentException if the requested lens (e.g., front camera) is not available on the device, or an IllegalStateException if the use cases are already bound or the lifecycle is invalid. Wrapping this call in a try-catch block prevents crashes on devices lacking front/back cameras or when binding fails.

        // RESUMED before binding so CameraX starts the session immediately.
        lifecycleRegistry.currentState = Lifecycle.State.RESUMED
        try {
            val cam = provider.bindToLifecycle(this, selector, preview, analysis)
            camera = cam
            boundUseCases = listOf(preview, analysis)

            // Re-apply a torch request made before/while the camera was down
            // (old zxing setTorch() had the same queued-until-open behavior).
            if (desiredTorchOn) {
                cam.cameraControl.enableTorch(true)
            }
        } catch (e: Exception) {
            camera = null
            boundUseCases = emptyList()
        }

Comment on lines 673 to 690
private fun startScan(arguments: List<Int>?, result: MethodChannel.Result) {
checkAndRequestPermission()

val allowedBarcodeTypes = getAllowedBarcodeTypes(arguments, result)

if (arguments == null) {
barcodeView?.decoderFactory = DefaultDecoderFactory(null, null, null, 2)
} else {
barcodeView?.decoderFactory = DefaultDecoderFactory(allowedBarcodeTypes, null, null, 2)
}

barcodeView?.decodeContinuous(
object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult) {
if (allowedBarcodeTypes.isEmpty() || allowedBarcodeTypes.contains(result.barcodeFormat)) {
val code = mapOf(
"code" to result.text,
"type" to result.barcodeFormat.name,
"rawBytes" to result.rawBytes
)
allowedFormats =
if (arguments.isNullOrEmpty()) ALL_CLASSIC_FORMATS
// May map to an empty set (e.g. only upcEanExtension was
// requested); the analyzer then decodes nothing, which is what
// the old decoder did with that same configuration.
else arguments.mapNotNull { DART_INDEX_TO_FORMAT[it] }.toSet()

channel.invokeMethod(CHANNEL_METHOD_ON_RECOGNIZE_QR, code)
}
}
scanning = true

override fun possibleResultPoints(resultPoints: List<ResultPoint>) = Unit
}
)
if (hasCameraPermission) {
ensureCameraStarted()
}
// If permission is not granted yet, onRequestPermissionsResult starts
// the camera as soon as the user allows it.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The startScan method does not invoke result.success(...) or result.error(...). This will cause the Dart-side future returned by _channel.invokeMethod('startScan', ...) to hang indefinitely on Android. Calling result.success(null) at the end of the method ensures the Dart future completes successfully.

Suggested change
private fun startScan(arguments: List<Int>?, result: MethodChannel.Result) {
checkAndRequestPermission()
val allowedBarcodeTypes = getAllowedBarcodeTypes(arguments, result)
if (arguments == null) {
barcodeView?.decoderFactory = DefaultDecoderFactory(null, null, null, 2)
} else {
barcodeView?.decoderFactory = DefaultDecoderFactory(allowedBarcodeTypes, null, null, 2)
}
barcodeView?.decodeContinuous(
object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult) {
if (allowedBarcodeTypes.isEmpty() || allowedBarcodeTypes.contains(result.barcodeFormat)) {
val code = mapOf(
"code" to result.text,
"type" to result.barcodeFormat.name,
"rawBytes" to result.rawBytes
)
allowedFormats =
if (arguments.isNullOrEmpty()) ALL_CLASSIC_FORMATS
// May map to an empty set (e.g. only upcEanExtension was
// requested); the analyzer then decodes nothing, which is what
// the old decoder did with that same configuration.
else arguments.mapNotNull { DART_INDEX_TO_FORMAT[it] }.toSet()
channel.invokeMethod(CHANNEL_METHOD_ON_RECOGNIZE_QR, code)
}
}
scanning = true
override fun possibleResultPoints(resultPoints: List<ResultPoint>) = Unit
}
)
if (hasCameraPermission) {
ensureCameraStarted()
}
// If permission is not granted yet, onRequestPermissionsResult starts
// the camera as soon as the user allows it.
}
private fun startScan(arguments: List<Int>?, result: MethodChannel.Result) {
checkAndRequestPermission()
allowedFormats =
if (arguments.isNullOrEmpty()) ALL_CLASSIC_FORMATS
// May map to an empty set (e.g. only upcEanExtension was
// requested); the analyzer then decodes nothing, which is what
// the old decoder did with that same configuration.
else arguments.mapNotNull { DART_INDEX_TO_FORMAT[it] }.toSet()
scanning = true
if (hasCameraPermission) {
ensureCameraStarted()
}
// If permission is not granted yet, onRequestPermissionsResult starts
// the camera as soon as the user allows it.
result.success(null)
}

Comment on lines +458 to +463
val results = try {
barcodeReader.read(img)
} catch (e: Exception) {
// Defensive: a single bad frame must not kill the analyzer.
emptyList()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since zxing-cpp relies on native C++ libraries, calls to barcodeReader.read(img) can potentially throw a Throwable (such as UnsatisfiedLinkError or other linkage/native errors) rather than just a standard Exception. Catching Throwable instead of Exception ensures that any native library loading or linking issues do not crash the application.

Suggested change
val results = try {
barcodeReader.read(img)
} catch (e: Exception) {
// Defensive: a single bad frame must not kill the analyzer.
emptyList()
}
val results = try {
barcodeReader.read(img)
} catch (e: Throwable) {
// Defensive: a single bad frame or native error must not kill the analyzer.
emptyList()
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pubspec.yaml (1)

15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the intent of the Flutter SDK constraint.

Flutter tooling enforces only the lower bound of environment.flutter, so ^3.44.0 does not provide an effective <4.0.0 upper bound. If this package simply requires Flutter 3.44+, retain >=3.44.0; otherwise verify the intended compatibility policy. (dart.dev)

🤖 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 `@pubspec.yaml` at line 15, Update the environment.flutter constraint to
reflect the intended compatibility policy: if the package only requires Flutter
3.44 or newer, replace ^3.44.0 with >=3.44.0; otherwise set an explicit upper
bound that tooling will enforce, based on verified supported versions.
🤖 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.

Nitpick comments:
In `@pubspec.yaml`:
- Line 15: Update the environment.flutter constraint to reflect the intended
compatibility policy: if the package only requires Flutter 3.44 or newer,
replace ^3.44.0 with >=3.44.0; otherwise set an explicit upper bound that
tooling will enforce, based on verified supported versions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9fed62db-38b6-4880-bfaf-143f3c2cbf87

📥 Commits

Reviewing files that changed from the base of the PR and between 3553810 and 4b2e93c.

📒 Files selected for processing (16)
  • android/build.gradle
  • android/src/main/AndroidManifest.xml
  • android/src/main/kotlin/net/touchcapture/qr/flutterqrplus/CustomFramingRectBarcodeView.kt
  • android/src/main/kotlin/net/touchcapture/qr/flutterqrplus/QRView.kt
  • example/lib/main.dart
  • lib/src/lifecycle_event_handler.dart
  • lib/src/platform/platform_info_stub.dart
  • lib/src/platform/platform_info_vm.dart
  • lib/src/qr_code_scanner.dart
  • lib/src/qr_scanner_overlay_shape.dart
  • lib/src/types/barcode_format.dart
  • lib/src/types/camera.dart
  • lib/src/types/features.dart
  • lib/src/web/flutter_qr_stub.dart
  • lib/src/web/flutter_qr_web.dart
  • pubspec.yaml
💤 Files with no reviewable changes (1)
  • android/src/main/kotlin/net/touchcapture/qr/flutterqrplus/CustomFramingRectBarcodeView.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant