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.

Key Takeaways

  • All three frameworks use the same Dynamsoft Capture Vision architecture (CameraEnhancer + CaptureVisionRouter), but the API surface differs significantly in verbosity and type safety
  • Flutter and React Native share a near-identical SDK pattern with singleton instances and callback-based receivers; .NET MAUI uses C# interfaces and instance-based construction
  • React Native relies on absolutely-positioned Views for overlay drawing, Flutter uses CustomPainter, and .NET MAUI uses SkiaSharp — each with different coordinate-mapping complexity
  • Flutter’s FittedBox.cover handles coordinate mapping automatically, while React Native and .NET MAUI require manual scale calculations
  • For inventory and batch-scanning scenarios, Flutter’s built-in state management (Provider) and dual-view mode offer the most feature-complete starting point

Common Developer Questions

  • “Which cross-platform framework is best for real-time barcode scanning on mobile?”
  • “How do I draw barcode bounding boxes as an overlay in React Native vs Flutter vs MAUI?”
  • “How do I handle camera coordinate-to-screen coordinate mapping for barcode overlays?”
  • “Which framework requires the least code to build a barcode scanner with Dynamsoft?”
  • “How does overlay rendering performance compare across Flutter, React Native, and MAUI?”

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);
  }
}

React Native — called at module level before component render:

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

.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);

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

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"
  }
}

.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>

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();
}

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]);

.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);
}

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));
    }
  }
}

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))
      ],
    ),
  ),
);

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 }]}
        />
      ))}
    </>
  );
};

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
}));

.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);
                }
            }
        }
    }
}

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;
  }
}

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();
  }
});

.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
})

Received in the page:

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

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

Common Issues & Edge Cases

  • Android portrait coordinate rotation: On Android in portrait mode, the Flutter SDK returns barcode coordinates in the camera’s native landscape orientation. The Flutter project includes a rotate90barcode() function (currently a pass-through) and the FittedBox.cover approach handles the visual mapping. If you render coordinates without accounting for orientation, bounding boxes will appear misaligned.
  • React Native overlay anti-aliasing: Drawing diagonal lines using absolutely-positioned View components produces jagged edges because CSS transforms on View elements do not support anti-aliasing. For production apps requiring smooth overlays, consider a native canvas module or SVG library.
  • React Native coordinate scaling with cropping: The camera preview uses “fill” mode (similar to aspectFill), which crops the camera frame. The overlay code must calculate both a scale factor and a crop offset to map barcode coordinates from camera space to screen space. Incorrect offsets cause overlays to drift away from actual barcode positions.
  • .NET MAUI cross-thread result access: The OnDecodedBarcodesReceived callback fires on a background thread, but SkiaSharp’s InvalidateSurface() must update the UI. The sample uses MainThread.BeginInvokeOnMainThread() combined with a lock object to safely pass barcode results to the paint callback. Missing either the dispatch or the lock causes race conditions.

Conclusion

For mobile barcode scanning apps, Flutter offers the best balance of SDK simplicity, automatic coordinate mapping, and feature completeness — its dual-view mode, settings screen, and scan history make it the most production-ready starting point. React Native is the most concise — the entire scanner fits in a single component — but its View-based overlay approach has visual limitations due to lack of anti-aliasing. .NET MAUI provides the most professional overlay rendering with SkiaSharp, and uses instance-based patterns that scale well for larger apps, at the cost of manual coordinate mapping and explicit thread synchronization. Regardless of framework choice, all three leverage the same Dynamsoft Capture Vision decoding engine, so barcode recognition speed and accuracy are consistent across implementations. For your next step, explore the Dynamsoft Capture Vision documentation to configure barcode formats, enable MRZ scanning, or add document detection to any of these implementations.

Source Code