DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

Dynamsoft Barcode SDK: Build Mobile Barcode Scanners with React Native, Flutter, and .NET MAUI

Choosing the right cross-platform framework for a mobile barcode scanning app means weighing API ergonomics against rendering performance and developer experience. This article compares React Native, Flutter, and .NET MAUI side by side on Android and iOS, each integrated with the Dynamsoft Capture Vision SDK, to show how they differ in setup, camera control, barcode decoding, and overlay rendering when building real-time scanning apps.

Dynamsoft Barcode SDK: React Native vs Flutter vs .NET MAUI comparison

What you'll build: A real-time mobile barcode scanner that decodes barcodes from a live camera feed and draws bounding-box overlays on each detected code, implemented in three frameworks using the Dynamsoft Capture Vision SDK.

Prerequisites

Framework SDK Version Dev Environment
Flutter dynamsoft_capture_vision_flutter 3.4.1200 Flutter 3.x, Dart SDK ≥3.3.0
React Native dynamsoft-capture-vision-react-native 3.4.1200 Node ≥18, React Native 0.79
.NET MAUI Dynamsoft.CaptureVisionBundle.Maui 3.4.1200 .NET 10 SDK, Visual Studio 2022

Get a 30-day free trial license for Dynamsoft Barcode Reader.

Initialize the Dynamsoft Barcode SDK License

All three frameworks require license initialization before any scanning can proceed, but the placement and error handling differ.

Flutter — called in the root widget's initState():

Future<void> _initLicense() async {
  try {
    final licenseResult = await LicenseManager.initLicense(
        'LICENSE-KEY');
    if (kDebugMode) print('License init: $licenseResult');
  } catch (e) {
    if (kDebugMode) print(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

React Native — called at module level before component render:

const License = 'LICENSE-KEY';
LicenseManager.initLicense(License).catch(e => {
  Alert.alert('License error', e.message);
});
Enter fullscreen mode Exit fullscreen mode

.NET MAUI — called in MauiProgram.cs during app startup. The mobile SDK uses the same LicenseManager from the Capture Vision Bundle:

string license = "LICENSE-KEY";
string errorMsg;
int errorCode = LicenseManager.InitLicense(license, out errorMsg);
if (errorCode != (int)Dynamsoft.Core.EnumErrorCode.EC_OK)
    Debug.WriteLine("License initialization error: " + errorMsg);
Enter fullscreen mode Exit fullscreen mode

Comparison: Flutter and React Native both use async/await patterns with exceptions; .NET MAUI uses a synchronous call with error codes. React Native's module-level initialization is the most concise, while Flutter's widget-lifecycle approach gives more control over timing. .NET MAUI's synchronous approach is straightforward but requires platform-specific placement.

Install and Configure the SDK

Flutter — a single package covers all Dynamsoft functionality:

# pubspec.yaml
dependencies:
  dynamsoft_capture_vision_flutter: ^3.4.1200
  provider: ^6.1.5+1
  url_launcher: ^6.3.2
  share_plus: ^12.0.1
  image_picker: ^1.2.1
  flutter_exif_rotation: ^0.5.2
Enter fullscreen mode Exit fullscreen mode

React Native — two packages: the core router and the barcode reader bundle:

{
  "dependencies": {
    "dynamsoft-capture-vision-react-native": "^3.4.1200",
    "dynamsoft-barcode-reader-bundle-react-native": "^11.4.1200",
    "react": "19.0.0",
    "react-native": "0.79.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

.NET MAUI — the Dynamsoft.CaptureVisionBundle.Maui package covers Android and iOS:

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-android36.0' Or '$(TargetFramework)' == 'net10.0-ios26.0'">
    <PackageReference Include="Dynamsoft.CaptureVisionBundle.Maui" Version="3.4.1200" />
</ItemGroup>

<ItemGroup>
    <PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="3.116.1" />
    <PackageReference Include="CommunityToolkit.Maui" Version="9.0.1" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

Comparison: Flutter wins on simplicity — one package, one import. React Native's two-package split is still straightforward. .NET MAUI sits in the middle with a single Capture Vision Bundle but needs additional dependencies like SkiaSharp for overlay drawing.

Set Up the Camera and Start Capturing

All three frameworks follow the same Capture Vision architecture: create a CameraEnhancer, create a CaptureVisionRouter, bind them together, and start capturing with a preset template.

Flutter:

Future<void> _sdkInit() async {
  _cvr = CaptureVisionRouter.instance;
  _cameraEnhancer = CameraEnhancer.instance;

  SimplifiedCaptureVisionSettings? currentSettings =
      await _cvr.getSimplifiedSettings(EnumPresetTemplate.readBarcodes);
  currentSettings!.barcodeSettings!.barcodeFormatIds =
      EnumBarcodeFormat.all;
  currentSettings.barcodeSettings!.expectedBarcodesCount = 0;
  await _cvr.updateSettings(EnumPresetTemplate.readBarcodes, currentSettings);

  _cvr.setInput(_cameraEnhancer);

  final CapturedResultReceiver receiver = CapturedResultReceiver()
    ..onDecodedBarcodesReceived = (DecodedBarcodesResult result) async {
      List<BarcodeResultItem>? res = result.items;
      if (mounted) {
        decodeRes = res ?? [];
        setState(() {});
      }
    };
  _cvr.addResultReceiver(receiver);

  await _cvr.startCapturing(EnumPresetTemplate.readBarcodes);
  await _cameraEnhancer.open();
}
Enter fullscreen mode Exit fullscreen mode

React Native:

const cvr = CaptureVisionRouter.getInstance();
const camera = CameraEnhancer.getInstance();

useEffect(() => {
  CameraEnhancer.requestCameraPermission();
  cvr.setInput(camera);
  camera.setCameraView(cameraView.current);

  const receiver = cvr.addResultReceiver({
    onDecodedBarcodesReceived: ({ items }) => {
      if (items?.length) {
        const overlays = items.map((item) => ({
          id: `${item.text}-${item.formatString}`,
          text: item.text,
          formatString: item.formatString,
          corners: item.location.points.map(point => ({
            x: point.x * scaleFactor - cropOffsetX,
            y: point.y * scaleFactor - cropOffsetY
          })),
        }));
        setBarcodeOverlays(overlays);
      }
    },
  });

  camera.open();
  cvr.startCapturing(EnumPresetTemplate.PT_READ_BARCODES)
    .catch(e => Alert.alert('Start error', e.message));

  return () => {
    cvr.stopCapturing();
    camera.close();
    cvr.removeResultReceiver(receiver);
  };
}, [cvr, camera]);
Enter fullscreen mode Exit fullscreen mode

.NET MAUI (Android/iOS):

public AndroidCameraPage()
{
    InitializeComponent();

    CameraPreview = new Dynamsoft.CameraEnhancer.Maui.CameraView();
    CameraPreview.SizeChanged += OnImageSizeChanged;
    MainGrid.Children.Insert(0, CameraPreview);

    enhancer = new CameraEnhancer();
    router = new CaptureVisionRouter();
    router.SetInput(enhancer);
    router.AddResultReceiver(this);
}

protected override void OnHandlerChanged()
{
    base.OnHandlerChanged();
    if (this.Handler != null && enhancer != null)
    {
        enhancer.SetCameraView(CameraPreview);
        enhancer.Open();
    }
}

protected override async void OnAppearing()
{
    base.OnAppearing();
    await Permissions.RequestAsync<Permissions.Camera>();
    router?.StartCapturing(EnumPresetTemplate.PT_READ_BARCODES, this);
}
Enter fullscreen mode Exit fullscreen mode

Comparison: Flutter and React Native use singleton instances (CaptureVisionRouter.instance, CaptureVisionRouter.getInstance()) with callback-based result receivers. .NET MAUI uses instance construction (new CaptureVisionRouter()) and C# interface-based callbacks (ICapturedResultReceiver). Flutter and React Native initialize the camera in the same call chain as the router; .NET MAUI requires the camera view to be set in OnHandlerChanged() after the native handler is ready.

Render Barcode Overlays on the Camera Feed

Overlay rendering is where the frameworks diverge most significantly. Each uses a fundamentally different drawing approach.

Flutter uses CustomPaint with a CustomPainter that draws directly on a Canvas:

class OverlayPainter extends CustomPainter {
  final List<BarcodeResultItem> results;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 30
      ..style = PaintingStyle.stroke;

    for (var result in results) {
      List<Point> points = result.location.points;

      canvas.drawLine(
          Offset(points[0].x.toDouble(), points[0].y.toDouble()),
          Offset(points[1].x.toDouble(), points[1].y.toDouble()), paint);
      canvas.drawLine(
          Offset(points[1].x.toDouble(), points[1].y.toDouble()),
          Offset(points[2].x.toDouble(), points[2].y.toDouble()), paint);
      canvas.drawLine(
          Offset(points[2].x.toDouble(), points[2].y.toDouble()),
          Offset(points[3].x.toDouble(), points[3].y.toDouble()), paint);
      canvas.drawLine(
          Offset(points[3].x.toDouble(), points[3].y.toDouble()),
          Offset(points[0].x.toDouble(), points[0].y.toDouble()), paint);

      TextPainter textPainter = TextPainter(
        text: TextSpan(
          text: result.text,
          style: const TextStyle(color: Colors.yellow, fontSize: 100.0),
        ),
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
      );
      textPainter.layout(minWidth: 0, maxWidth: size.width);
      textPainter.paint(canvas, Offset(minX, minY));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The camera view is wrapped in a FittedBox with BoxFit.cover, and the overlay is positioned on top:

SizedBox(
  width: screenWidth,
  height: screenHeight,
  child: FittedBox(
    fit: BoxFit.cover,
    child: Stack(
      children: [
        SizedBox(
          width: isPortrait ? _previewHeight : _previewWidth,
          height: isPortrait ? _previewWidth : _previewHeight,
          child: CameraView(cameraEnhancer: _cameraEnhancer),
        ),
        Positioned(
            left: 0, top: 0, right: 0, bottom: 0,
            child: createOverlay(decodeRes))
      ],
    ),
  ),
);
Enter fullscreen mode Exit fullscreen mode

React Native draws overlays using absolutely-positioned View components — no canvas API is needed:

const BarcodeContour: React.FC<{ corners: Array<{ x: number, y: number }> }> = ({ corners }) => {
  if (corners.length !== 4) return null;

  return (
    <>
      {corners.map((corner, index) => {
        const nextCorner = corners[(index + 1) % 4];
        const deltaX = nextCorner.x - corner.x;
        const deltaY = nextCorner.y - corner.y;
        const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
        const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);

        return (
          <View
            key={index}
            style={[
              styles.contourLine,
              {
                left: corner.x,
                top: corner.y - 1.5,
                width: length,
                height: 3,
                transform: [{ rotate: `${angle}deg` }],
              },
            ]}
          />
        );
      })}
      {corners.map((corner, index) => (
        <View
          key={`dot-${index}`}
          style={[styles.cornerDot, { left: corner.x - 4, top: corner.y - 4 }]}
        />
      ))}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Coordinate mapping from camera space to screen space is handled manually:

const scaleFactorX = cameraViewWidth / cameraResolutionWidth;
const scaleFactorY = cameraViewHeight / cameraResolutionHeight;
const scaleFactor = Math.max(scaleFactorX, scaleFactorY);

const scaledCameraWidth = cameraResolutionWidth * scaleFactor;
const scaledCameraHeight = cameraResolutionHeight * scaleFactor;
const cropOffsetX = (scaledCameraWidth - viewWidth) / 2;
const cropOffsetY = (scaledCameraHeight - viewHeight) / 2;

const scaledCorners = item.location.points.map(point => ({
  x: (point.x * scaleFactor) - cropOffsetX,
  y: (point.y * scaleFactor) - cropOffsetY
}));
Enter fullscreen mode Exit fullscreen mode

.NET MAUI (iOS) uses SkiaSharp (SKCanvasView) for overlay rendering. The Android implementation uses a GraphicsView with a custom Drawable, while iOS uses SkiaSharp for precise drawing:

public void OnDecodedBarcodesReceived(DecodedBarcodesResult result)
{
    if (imageWidth == 0 && imageHeight == 0)
    {
        IntermediateResultManager manager = router.GetIntermediateResultManager();
        ImageData data = manager.GetOriginalImage(result.OriginalImageHashId);
        imageWidth = data.Width;
        imageHeight = data.Height;
    }

    lock (_lockObject)
    {
        _barcodeResult = result;
        CameraPreview.GetDrawingLayer(EnumDrawingLayerId.DLI_DBR).Visible = false;
        MainThread.BeginInvokeOnMainThread(() =>
        {
            canvasView.InvalidateSurface();
        });
    }
}

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKCanvas canvas = args.Surface.Canvas;
    canvas.Clear();

    SKPaint skPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 4,
    };

    SKPaint textPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 4,
    };

    float textSize = 18;
    SKFont font = new SKFont() { Size = textSize };

    lock (_lockObject)
    {
        if (_barcodeResult != null)
        {
            var items = _barcodeResult.Items;
            if (items != null)
            {
                foreach (var barcodeItem in items)
                {
                    Microsoft.Maui.Graphics.Point[] points = barcodeItem.Location.Points;
                    float x1 = (float)(points[0].X / scale);
                    float y1 = (float)(points[0].Y / scale);
                    float x2 = (float)(points[1].X / scale);
                    float y2 = (float)(points[1].Y / scale);
                    float x3 = (float)(points[2].X / scale);
                    float y3 = (float)(points[2].Y / scale);
                    float x4 = (float)(points[3].X / scale);
                    float y4 = (float)(points[3].Y / scale);

                    canvas.DrawText(barcodeItem.Text, x1, y1 - 10, SKTextAlign.Left, font, textPaint);
                    canvas.DrawLine(x1, y1, x2, y2, skPaint);
                    canvas.DrawLine(x2, y2, x3, y3, skPaint);
                    canvas.DrawLine(x3, y3, x4, y4, skPaint);
                    canvas.DrawLine(x4, y4, x1, y1, skPaint);
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Comparison:

Aspect Flutter React Native .NET MAUI
Drawing engine CustomPainter / Canvas Absolute-positioned Views SkiaSharp / GraphicsView Drawable
Coordinate mapping FittedBox.cover handles it automatically Manual scale + crop offset calculation Manual scale calculation with orientation handling
Built-in overlay option No No No
Thread safety Single-threaded (UI thread) Single-threaded (JS thread) Requires lock for cross-thread result access
Overlay complexity Low (Canvas API) Medium (CSS-style transforms for lines) Medium (SkiaSharp, clean API, orientation logic)

Handle App Lifecycle and Camera State

Managing camera start/stop when the app goes to background is critical for resource management and battery life.

Flutter uses WidgetsBindingObserver:

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  super.didChangeAppLifecycleState(state);
  switch (state) {
    case AppLifecycleState.resumed:
      start();
      break;
    case AppLifecycleState.inactive:
      stop();
      break;
    default:
      break;
  }
}
Enter fullscreen mode Exit fullscreen mode

React Native uses AppState:

const sub = AppState.addEventListener('change', nextState => {
  if (nextState === 'active') {
    camera.open();
    cvr.startCapturing(EnumPresetTemplate.PT_READ_BARCODES);
  } else if (nextState.match(/inactive|background/)) {
    cvr.stopCapturing();
    camera.close();
  }
});
Enter fullscreen mode Exit fullscreen mode

.NET MAUI (Android) uses WeakReferenceMessenger with lifecycle events registered in MauiProgram.cs:

.ConfigureLifecycleEvents(events =>
{
#if ANDROID
    events.AddAndroid(android => android
        .OnResume((activity) => NotifyPage("Resume"))
        .OnStop((activity) => NotifyPage("Stop")));
#endif
})
Enter fullscreen mode Exit fullscreen mode

Received in the page:

WeakReferenceMessenger.Default.Register<LifecycleEventMessage>(this, (r, message) =>
{
    if (message.EventName == "Resume") enhancer?.Open();
    else if (message.EventName == "Stop") enhancer?.Close();
});
Enter fullscreen mode Exit fullscreen mode

Comparison: Flutter and React Native handle lifecycle directly in the scanning component. .NET MAUI's messenger pattern decouples lifecycle detection from the page, which is cleaner for larger apps but adds indirection for simple use cases.

Compare SDK Architecture and API Patterns

Feature Flutter React Native .NET MAUI
Router creation CaptureVisionRouter.instance (singleton) CaptureVisionRouter.getInstance() (singleton) new CaptureVisionRouter() (instance)
Camera creation CameraEnhancer.instance (singleton) CameraEnhancer.getInstance() (singleton) new CameraEnhancer() (instance)
Result receiver Callback object Callback object ICapturedResultReceiver interface
Camera view CameraView(cameraEnhancer: ...) <CameraView ref={...} /> Dynamsoft.CameraEnhancer.Maui.CameraView()
Start capturing startCapturing(template) async startCapturing(template) async StartCapturing(template, listener)
Overlay library Built-in Canvas Built-in Views SkiaSharp (3rd-party)

Flutter and React Native use singleton patterns, which simplifies access across widgets but can complicate testing. .NET MAUI uses instance construction, which is more testable and explicit but requires managing object lifetimes.

Compare Framework Metrics Side by Side

Metric Flutter React Native .NET MAUI
SDK packages 1 2 1 + SkiaSharp
Camera setup 3 lines 3 lines 5 lines
Overlay approach CustomPainter Absolutely-positioned Views SkiaSharp
Coordinate mapping Automatic (FittedBox) Manual Manual (with orientation handling)
Router pattern Singleton Singleton Instance
State management Provider (built in) useState hooks CommunityToolkit.Mvvm
Thread safety UI thread only JS thread only Requires explicit locking

Source Code

Top comments (0)