How to Build an Ionic Vue QR Code Scanner with the Dynamsoft Capture Vision Native SDK

Decoding QR codes and 1D barcodes from a phone camera needs a production-grade native pipeline. With the Dynamsoft Capture Vision native SDK (com.dynamsoft:capturevisionbundle:3.6.2000 on Android, DynamsoftCaptureVisionBundle 3.6.2000 on iOS) wrapped in a small in-app Capacitor plugin, every frame is processed on-device with the multi-format preset template and the result is returned as JSON. This article walks through the ionic-vue-qr-code-scanner reference project — one Ionic Vue app, one in-app plugin, two data sources, and zero JavaScript scanning SDK.

The scanner supports QR Code, DataMatrix, PDF417, Aztec, Code 39/93/128, EAN, UPC, ITF and Codabar, with real-time contour drawing on the native preview. The same code path accepts a still image from the gallery so a user can pick a saved photo and decode it with the same multi-format pipeline.

What you’ll build

  • An Ionic Vue app that exposes two clearly separated data sources — Camera and File — with the live preview running entirely on the native side.
  • A multi-format barcode scanner that runs the built-in barcode preset template (ReadBarcodes on Android, ReadBarcodes_Default after loading the SDK templates on iOS) on every camera frame and overlays the decoded contour through Dynamsoft’s DrawingLayer system on both platforms (DBR_LAYER_ID on Android, a custom green-styled layer on iOS).
  • An in-app Capacitor plugin (BarcodeScannerNative) that returns every decoded barcode with text, format, format string and four corner points, ready to be rendered as a list.
  • A matching iOS Swift implementation that drops into the same Xcode project once the Dynamsoft pods are installed via pod install (see Step 4).

Key Takeaways

  • Native SDK end-to-end. All decoding happens on-device. The WebView only renders the result; it never loads a JavaScript scanner.
  • Camera + file, one engine. Both data sources feed the same CaptureVisionRouter through the same ScannerEngine singleton so the deep-learning models load only once.
  • Latest Capture Vision release. Both platforms use the same 3.6.2000 SDK (com.dynamsoft:capturevisionbundle and DynamsoftCaptureVisionBundle).
  • No third-party plugin. The Capacitor bridge is fully implemented in this project.

Architecture

Ionic QR code scanner data flow

Common Developer Questions

What barcode formats are supported?

QR Code, DataMatrix, PDF417, Aztec, Code 39, Code 93, Code 128, EAN-8, EAN-13, UPC-A, UPC-E, ITF and Codabar. The ReadBarcodes preset template enables the entire set out of the box — no extra configuration is required.

No. The native plugin implements its own gallery picker on Android (an ACTION_PICK intent through MediaStore) and on iOS (UIImagePickerController after requesting photo-library access). Adding @capacitor/camera would introduce a second picker that the user has to authorise separately. The reference project keeps the picker inside the plugin for exactly this reason.

Why does the camera page not show an HTML video tag?

DSCameraView (iOS) and CameraView (Android) are native views from the Camera Enhancer module. They run AVFoundation and CameraX respectively, which gives the scanner access to frame-accurate control and the drawing layer for real-time contour rendering. An HTML <video> element would not work — the JS SDK is intentionally not used in this project.

How does the camera result differ from the file result?

The camera path also runs the camera frame through the multi-frame result filter, so contours stay on screen until the user confirms. The file path returns whatever the SDK can decode from the supplied still image; if no barcodes are present, the call rejects with "No barcodes were found in the image.".

Prerequisites

  • Node.js 20+ and npm
  • Android Studio with Android SDK 36 (compileSdk 36, minSdk 24)
  • Xcode 16+ (or newer) for iOS
  • A physical device — the camera does not work in the iOS simulator or the Android emulator
  • A Dynamsoft Capture Vision trial license (already embedded in android/app/src/main/res/values/strings.xml and mirrored in the iOS plugin source)

Get a 30-day free trial license

Step 1: Scaffold the project

git clone https://github.com/yushulx/android-camera-barcode-mrz-document-scanner.git
cd android-camera-barcode-mrz-document-scanner/examples/ionic-vue-qr-code-scanner
npm install
npm run build
npx cap sync
npx cap open android    # or `npx cap open ios`

The Vite + Vue 3 build outputs the SPA in dist/. Capacitor copies that directory into the native assets so the WebView loads it offline.

Step 2: The web layer

The web layer exports the plugin contract and renders the two data sources as separate cards.

// src/scanner/BarcodeScannerNative.ts
import { registerPlugin } from '@capacitor/core';

export interface BarcodeResult {
  text: string;
  format: string;
  formatString: string;
  points: Array<{ x: number; y: number }>;
}

export interface BarcodeScannerNativePlugin {
  initLicense(): Promise<{ success: boolean; message: string }>;
  startScan(): Promise<{ results: BarcodeResult[] }>;
  scanFromGallery(): Promise<{ results: BarcodeResult[] }>;
  scanFile(options: { uri: string }): Promise<{ results: BarcodeResult[] }>;
}

export const BarcodeScannerNative = registerPlugin<BarcodeScannerNativePlugin>('BarcodeScannerNative');

HomePage.vue shows two cards — Camera and File — and a spinner while the native scanner is running. The result is stored in a tiny reactive store and the app navigates to a result view that lists every decoded barcode. Each entry shows the format, the decoded text, and (when available) the four corner points.

Step 3: The native plugin on Android

The in-app plugin lives at android/app/src/main/java/com/dynamsoft/ionic/qrcodescanner/. MainActivity registers the plugin class with the Capacitor bridge.

public class MainActivity extends BridgeActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        registerPlugin(BarcodeScannerNativePlugin.class);
        super.onCreate(savedInstanceState);
    }
}

ScannerEngine owns a single CaptureVisionRouter and the license state. The startCapturing overload uses the built-in PT_READ_BARCODES preset template.

public static String template() {
    return EnumPresetTemplate.PT_READ_BARCODES;
}

public DecodedBarcodesResult decodeFile(String path) throws ScannerException {
    return unwrap(router.capture(path, template()));
}

ScannerActivity renders the live preview with a com.dynamsoft.dce.CameraView and registers a CapturedResultReceiver whose onDecodedBarcodesReceived callback is fired on every frame that contains at least one barcode. Each item is drawn on the DBR_LAYER_ID through a QuadDrawingItem, so the user sees a green outline while aiming. The Confirm button serializes the latest items into a JSON array and finishes the activity.

The file data source uses engine.decodeFile / engine.decodeBitmap, which call CaptureVisionRouter.capture on a single still image. BarcodeScannerNativePlugin.decodeUri handles content:// and file:// URIs transparently by switching between engine.decodeBitmap and engine.decodeFile.

The app/build.gradle adds the capturevisionbundle dependency and the packagingOptions that keep the bundled .so files intact.

dependencies {
    // Dynamsoft Capture Vision native SDK (latest release)
    implementation 'com.dynamsoft:capturevisionbundle:3.6.2000'
}

packagingOptions {
    doNotStrip "**/*.so"
    pickFirst "**/libc++_shared.so"
}

The project root build.gradle adds the Dynamsoft Maven repository so the AAR can be resolved.

Step 4: The native plugin on iOS

The iOS side mirrors the Java plugin. Swift files in ios/App/App/ include the plugin (BarcodeScannerNativePlugin.swift), the camera view controller (BarcodeCameraScanViewController.swift) and a SharedImagePicker wrapper around UIImagePickerController that requests photo-library access first.

The plugin class is registered automatically because it appears in ios/App/App/capacitor.config.json. Note that on Capacitor 7+ the Swift class must also conform to CAPBridgedPlugin (declare identifier, jsName and pluginMethods) — a bare @objc subclass of CAPPlugin is silently skipped, and every web call then resolves to “plugin is not implemented”:

{
  "appId": "com.dynamsoft.ionic.qrcodescanner",
  "packageClassList": ["BarcodeScannerNativePlugin"]
}

The camera view controller uses CameraView + CameraEnhancer + CaptureVisionRouter from the DynamsoftCaptureVisionBundle CocoaPods framework and draws the contours through the SDK’s own drawing system — not a hand-drawn overlay. A custom layer created with cameraView.createDrawingLayer() receives QuadDrawingItems built from the decoded locations, and the SDK maps the video-frame coordinates (landscape pixel space) onto the portrait preview automatically.

let dce = CameraEnhancer()
let cvr = CaptureVisionRouter()
try? cvr.setInput(dce)
cvr.addResultReceiver(self)

// The iOS SDK ships its presets as a JSON template file inside
// DynamsoftCaptureVisionBundle.framework - load it once, then use the
// platform template name (ReadBarcodes_Default on iOS, ReadBarcodes on Android).
if let templates = Bundle.allFrameworks.lazy
    .compactMap({ $0.path(forResource: "dbr-bundle-mobile-templates", ofType: "json") })
    .first {
    try? cvr.initSettingsFromFile(templates)
}

try? cvr.startCapturing("ReadBarcodes_Default") { success, error in
    DispatchQueue.main.async {
        if !success { self.statusLabel.text = error?.localizedDescription }
    }
}

Contour drawing is wired through the SDK drawing layers:

private func configureDrawingLayers() {
    let barcodeStyle = DrawingStyleManager.createDrawingStyle(
        .green, strokeWidth: 3,
        fill: UIColor.green.withAlphaComponent(0.15),
        textColor: .white, font: .systemFont(ofSize: 12))
    let barcodeLayer = cameraView.createDrawingLayer()
    barcodeLayer.visible = true
    barcodeLayer.setDefaultStyle(barcodeStyle)
    barcodeLayerId = barcodeLayer.layerId
}

// Called with the decoded items of every frame (empty array when a frame has no results).
private func apply(items: [BarcodeResultItem]) {
    latestItems = items
    let layer = cameraView.getDrawingLayer(barcodeLayerId)
    layer?.clearDrawingItems()
    if !items.isEmpty {
        layer?.addDrawingItems(items.map { QuadDrawingItem(quadrilateral: $0.location) })
    }
    // ... status label + Confirm button state
}

Keep the contours on an SDK drawing layer. A custom UIView overlay defaults to isOpaque = true: once its draw(_:) strokes a path, the un-painted pixels of the backing store composite as opaque black and cover the camera preview the moment the first barcode is found — and the raw item.location points are video-frame coordinates that would need manual orientation and aspect-fill conversion anyway. The SDK drawing layer handles both problems.

The Swift SDK modules are integrated with CocoaPods (not Swift Package Manager). The ios/App/Podfile of the project looks like this:

target 'App' do
  capacitor_pods
  pod 'DynamsoftCaptureVisionBundle', '3.6.2000'   # self-contained: camera + core + license
end

The Capture Vision framework already ships DSCameraEnhancer / DSCameraView, so no separate DynamsoftCameraEnhancer pod is needed — adding one would register a second DSCameraEnhancer class and live frames would never reach the router (the camera would stay on “Scanning…” while file decoding still works). The bundle framework is also self-contained: it embeds the Core and License modules and its binary declares no load dependency on any other Dynamsoft framework, so DynamsoftCore / DynamsoftLicense must not be added as separate pods. Doing so registers dozens of duplicate Objective-C classes at launch (Class DSxxx is implemented in both ... One of the two will be used in the console) and dyld then picks one implementation at random, which breaks the camera preview and the drawing layers in hard-to-debug ways. Run pod install once before opening App.xcworkspace:

cd ios/App
pod install
open App.xcworkspace   # use the workspace, not the .xcodeproj

The Info.plist already declares NSCameraUsageDescription.

Step 5: Run it

# Android
npx cap run android --target=<device-id>

# iOS
npx cap run ios

When the camera scanner is on screen every detected barcode is outlined immediately, the Confirm button becomes enabled, and tapping it returns the full list back to the web layer. The file picker opens the system gallery; whichever image is picked is decoded with the same multi-format preset template and the result is rendered as a list.

Decoded list on a real device — UPC-A, UPC-E, QR Code (www.dynamsoft.com) and Code 128 captured in a single multi-format frame

Common Issues & Edge Cases

  • Nothing happens when the camera is opened. Check that the CAMERA permission is granted (the manifest already declares it, but the runtime grant happens on first use). On Android 13+ the READ_MEDIA_IMAGES permission is also required for the gallery picker.
  • The contour never shows up. The scanner only draws when at least one barcode is decoded in the current frame. Aim at a single barcode first, then try multi-barcode scenarios.
  • The preview turns black as soon as a contour is drawn (iOS). The overlay is not going through the SDK drawing layer. A hand-drawn UIView defaults to isOpaque = true, so the un-painted pixels of its backing store composite as opaque black over the preview once draw(_:) strokes the first path — and the raw item.location points are video-frame coordinates that need conversion. Use QuadDrawingItem on a CameraView drawing layer (see Step 4).
  • Class DSxxx is implemented in both ... at launch (iOS). Two copies of the SDK are linked — typically standalone DynamsoftCore / DynamsoftLicense pods next to the bundle pod. Remove them (see Step 4); duplicate class registration makes dyld pick one implementation at random.
  • The plugin call rejects with “canceled”. The user dismissed the system photo picker or the native camera scanner; no state was changed.
  • “Not implemented” in the web browser. That is intentional. The plugin is a native-only bridge; it has no web fallback in this project.

Source Code

Get the complete sample project source code on GitHub: examples/ionic-vue-qr-code-scanner