flutter_barcode_sdk 5.2.0 copy "flutter_barcode_sdk: ^5.2.0" to clipboard
flutter_barcode_sdk: ^5.2.0 copied to clipboard

The only Flutter barcode SDK for Android, iOS, Web, Windows, Linux, macOS. Empower developers to build 1D/2D barcode reader and barcode scanner.

example/lib/main.dart

// A unified cross-platform barcode scanner example for flutter_barcode_sdk.
//
// The camera preview and RGB frame capture are provided by
// `flutter_lite_camera`, so this single code base runs on Android, iOS, web,
// Windows, macOS and Linux without any platform-specific files.
//
// Features:
// - Live camera scanning with a real-time annotation overlay.
// - Resolution preset selection.
// - Image file scanning (file loading + decoding + annotated result view).

import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_barcode_sdk/flutter_barcode_sdk.dart';
import 'package:flutter_lite_camera/flutter_lite_camera.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  runApp(const BarcodeScannerApp());
}

class BarcodeScannerApp extends StatelessWidget {
  const BarcodeScannerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dynamsoft Barcode Reader',
      theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
      home: const ScannerHomePage(),
    );
  }
}

class ScannerHomePage extends StatefulWidget {
  const ScannerHomePage({super.key});

  @override
  State<ScannerHomePage> createState() => _ScannerHomePageState();
}

class _ScannerHomePageState extends State<ScannerHomePage> {
  final FlutterLiteCamera _camera = FlutterLiteCamera();

  // To read barcodes, get a 30-day FREE trial license for Dynamsoft Barcode
  // Reader:
  // https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform
  static const String _licenseKey =
      'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==';

  FlutterBarcodeSdk? _barcodeReader;
  bool _isSdkReady = false;

  bool _isCameraOpened = false;
  int _textureId = -1;
  // Actual frame size negotiated with the device. Filled in after the camera
  // opens (and after every resolution change) — never hard-coded.
  int _width = 0;
  int _height = 0;
  // Clockwise degrees to rotate the preview/result so it appears upright.
  // Phones report 90 (or 180/270) depending on how the device is held;
  // desktop/web report 0.
  int _rotation = 0;
  ResolutionPreset _preset = ResolutionPreset.medium;

  bool _shouldDecode = false;
  bool _isDecoding = false;
  List<BarcodeResult> _results = [];
  bool _isPickingFile = false;

  @override
  void initState() {
    super.initState();
    _initBarcodeSdk();
  }

  @override
  void dispose() {
    _stopCamera();
    super.dispose();
  }

  Future<void> _initBarcodeSdk() async {
    final reader = FlutterBarcodeSdk();
    await reader.setLicense(_licenseKey);
    await reader.init();
    if (mounted) {
      setState(() {
        _barcodeReader = reader;
        _isSdkReady = true;
      });
    }
  }

  // ---------------------------------------------------------------------------
  // Camera (flutter_lite_camera)
  // ---------------------------------------------------------------------------

  Future<void> _startCamera() async {
    try {
      List<String> devices = await _camera.getDeviceList();
      if (devices.isEmpty) {
        debugPrint('No camera devices found.');
        return;
      }
      debugPrint('Available devices: $devices');

      // false = permission denied or the device is busy/unavailable.
      bool opened = await _camera.open(0);
      if (!opened) {
        debugPrint('Failed to open the camera.');
        return;
      }

      // The native layer renders the video feed into this texture; no
      // frame data crosses into Dart for display purposes.
      int textureId = await _camera.startPreview();
      int rotation = 0;
      try {
        rotation = await _camera.getRotation();
      } catch (e) {
        debugPrint('getRotation failed, assuming 0: $e');
      }
      // The plugin negotiates the actual frame size with the device; read it
      // back instead of assuming a fixed resolution.
      int width = await _camera.getWidth();
      int height = await _camera.getHeight();
      debugPrint('Negotiated frame size: ${width}x$height, rotation $rotation');

      setState(() {
        _isCameraOpened = true;
        _textureId = textureId;
        if (width > 0 && height > 0) {
          _width = width;
          _height = height;
        }
        _rotation = rotation;
        _shouldDecode = true;
      });

      // Start pulling frames for barcode decoding only. This does not
      // affect the preview stream.
      _decodeFrames();

      // Honor a preset chosen before the camera was opened (open()
      // negotiates the medium target by default).
      if (_preset != ResolutionPreset.medium) {
        await _applyResolution(_preset);
      }
    } catch (e) {
      debugPrint('Error initializing camera: $e');
    }
  }

  Future<void> _stopCamera() async {
    _shouldDecode = false;

    if (_isCameraOpened) {
      try {
        await _camera.stopPreview();
        await _camera.release();
      } catch (e) {
        debugPrint('Failed to release the camera: $e');
      }
      if (mounted) {
        setState(() {
          _isCameraOpened = false;
          _textureId = -1;
          _results = [];
        });
      }
    }
  }

  Future<void> _applyResolution(ResolutionPreset preset) async {
    setState(() => _preset = preset);
    bool ok = await _camera.setResolutionPreset(preset);
    if (!ok) {
      debugPrint('setResolutionPreset($preset) failed.');
      return;
    }
    // The device may have fallen back to a different size; read the actual
    // one so the preview aspect ratio and overlay stay correct.
    int width = await _camera.getWidth();
    int height = await _camera.getHeight();
    debugPrint('Resolution after $preset: ${width}x$height');
    if (width > 0 && height > 0) {
      setState(() {
        _width = width;
        _height = height;
      });
    }
  }

  Future<void> _decodeFrames() async {
    if (!_isCameraOpened || !_shouldDecode) return;

    if (!_isDecoding && _barcodeReader != null) {
      _isDecoding = true;
      try {
        Map<String, dynamic> frame = await _camera.captureFrame();
        if (frame.containsKey('data')) {
          final int frameWidth = frame['width'];
          final int frameHeight = frame['height'];
          // Keep the preview aspect ratio and the overlay in sync with the
          // real frame size: the device may deliver frames at a different
          // size than the one negotiated at open() time.
          _width = frameWidth;
          _height = frameHeight;
          Uint8List rgbBuffer = frame['data'];

          List<BarcodeResult> ret = await _barcodeReader!.decodeImageBuffer(
            rgbBuffer,
            frameWidth,
            frameHeight,
            frameWidth * 3,
            ImagePixelFormat.IPF_RGB_888.index,
            ImageRotation.rotation0.value,
          );

          if (_shouldDecode && mounted) {
            setState(() {
              _results = ret;
            });
          }
        }
      } catch (e) {
        // No frame available yet.
      }
      _isDecoding = false;
    }

    if (_shouldDecode) {
      Future.delayed(const Duration(milliseconds: 30), _decodeFrames);
    }
  }

  // ---------------------------------------------------------------------------
  // Image file scanning (file loading + decoding + annotated result view)
  // ---------------------------------------------------------------------------

  Future<void> _scanImageFile() async {
    if (!_isSdkReady || _barcodeReader == null) {
      _showMessage('The barcode SDK is not ready yet.');
      return;
    }
    if (_isPickingFile) return;

    // Release the camera first: mobile platforms only allow one camera
    // client at a time.
    await _stopCamera();

    // image_picker opens the system photo library on iOS/Android (PHPicker /
    // Photo Picker) and falls back to a file dialog on desktop and web.
    final ImagePicker picker = ImagePicker();

    setState(() => _isPickingFile = true);
    try {
      final XFile? file =
          await picker.pickImage(source: ImageSource.gallery);
      if (file == null) return; // The user canceled the dialog.

      List<BarcodeResult> results;
      if (kIsWeb) {
        // The web SDK cannot read from a file path, so decode the raw RGBA
        // pixels instead.
        final Uint8List bytes = await file.readAsBytes();
        final ui.Image image = await decodeImageFromList(bytes);
        final ByteData? byteData =
            await image.toByteData(format: ui.ImageByteFormat.rawRgba);
        if (byteData == null) {
          throw StateError('Failed to read raw pixel data of the image.');
        }
        results = await _barcodeReader!.decodeImageBuffer(
          byteData.buffer.asUint8List(),
          image.width,
          image.height,
          image.width * 4,
          ImagePixelFormat.IPF_ARGB_8888.index,
          ImageRotation.rotation0.value,
        );
      } else {
        results = await _barcodeReader!.decodeFile(file.path);
      }

      if (!mounted) return;
      await Navigator.of(context).push(MaterialPageRoute<void>(
        builder: (_) => ScanResultScreen(file: file, results: results),
      ));
    } catch (e) {
      _showMessage('Failed to scan the image file: $e');
    } finally {
      if (mounted) setState(() => _isPickingFile = false);
    }
  }

  void _showMessage(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(message)));
  }

  // ---------------------------------------------------------------------------
  // UI
  // ---------------------------------------------------------------------------

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          if (_textureId >= 0)
            LayoutBuilder(
              builder: (context, constraints) {
                final double screenWidth = constraints.maxWidth;
                final double screenHeight = constraints.maxHeight;

                // When the preview is rotated 90/270 degrees the displayed
                // frame becomes portrait, so the aspect ratio flips. Fall
                // back to 4:3 until the first frame reports its real size.
                final int frameWidth = _width > 0 ? _width : 4;
                final int frameHeight = _height > 0 ? _height : 3;
                final bool rotated = _rotation % 180 == 90;
                final double imageAspectRatio = rotated
                    ? frameHeight / frameWidth
                    : frameWidth / frameHeight;
                final double screenAspectRatio = screenWidth / screenHeight;

                double drawWidth, drawHeight;
                if (imageAspectRatio > screenAspectRatio) {
                  drawWidth = screenWidth;
                  drawHeight = screenWidth / imageAspectRatio;
                } else {
                  drawHeight = screenHeight;
                  drawWidth = screenHeight * imageAspectRatio;
                }

                return Center(
                  child: SizedBox(
                    width: drawWidth,
                    height: drawHeight,
                    child: Stack(
                      children: [
                        // Rotate only the widget — never the native pixels —
                        // so the preview appears upright without any
                        // per-frame buffer processing.
                        RotatedBox(
                          quarterTurns: (_rotation ~/ 90) % 4,
                          child: _camera.buildPreview(_textureId),
                        ),
                        CustomPaint(
                          painter: ResultPainter(
                            _results,
                            srcWidth: frameWidth,
                            srcHeight: frameHeight,
                            drawWidth: drawWidth,
                            drawHeight: drawHeight,
                            rotation: _rotation,
                          ),
                          child: Container(),
                        ),
                      ],
                    ),
                  ),
                );
              },
            )
          else
            Center(
              child: _isCameraOpened
                  ? const CircularProgressIndicator()
                  : const Text('Camera not started'),
            ),
          Positioned(
            left: 20,
            right: 20,
            bottom: 20,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    ElevatedButton(
                      onPressed: _isCameraOpened ? null : _startCamera,
                      child: const Text('Start'),
                    ),
                    Expanded(
                      child: Center(
                        child: Container(
                          padding: const EdgeInsets.symmetric(horizontal: 12),
                          decoration: BoxDecoration(
                            color: Colors.white.withValues(alpha: 0.9),
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: DropdownButton<ResolutionPreset>(
                            value: _preset,
                            underline: const SizedBox.shrink(),
                            isExpanded: true,
                            onChanged: (preset) {
                              if (preset == null) return;
                              if (_isCameraOpened) {
                                _applyResolution(preset);
                              } else {
                                setState(() => _preset = preset);
                              }
                            },
                            items: ResolutionPreset.values
                                .map((p) => DropdownMenuItem(
                                      value: p,
                                      child: Text(
                                        '${p.name} (${p.width}x${p.height})',
                                        overflow: TextOverflow.ellipsis,
                                      ),
                                    ))
                                .toList(),
                          ),
                        ),
                      ),
                    ),
                    ElevatedButton(
                      onPressed: !_isCameraOpened ? null : _stopCamera,
                      child: const Text('Stop'),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                ElevatedButton.icon(
                  onPressed: _isPickingFile ? null : _scanImageFile,
                  icon: const Icon(Icons.image_outlined),
                  label: const Text('Scan Image File'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// An image file and its decoded in-memory representation.
class _LoadedImage {
  final Uint8List bytes;
  final ui.Image image;

  const _LoadedImage(this.bytes, this.image);
}

/// The result page for image file scanning: shows the loaded image with an
/// annotation overlay drawn on every detected barcode, plus the decoded
/// text below.
class ScanResultScreen extends StatefulWidget {
  final XFile file;
  final List<BarcodeResult> results;

  const ScanResultScreen({
    super.key,
    required this.file,
    required this.results,
  });

  @override
  State<ScanResultScreen> createState() => _ScanResultScreenState();
}

class _ScanResultScreenState extends State<ScanResultScreen> {
  late Future<_LoadedImage> _loadFuture;

  @override
  void initState() {
    super.initState();
    _loadFuture = _loadImage();
  }

  Future<_LoadedImage> _loadImage() async {
    final Uint8List bytes = await widget.file.readAsBytes();
    final ui.Image image = await decodeImageFromList(bytes);
    return _LoadedImage(bytes, image);
  }

  String get _title {
    final String name = widget.file.name;
    return name.isEmpty ? 'Scan Result' : name;
  }

  String get _summary {
    final List<BarcodeResult> results = widget.results;
    if (results.isEmpty) return 'No barcode detected';

    final StringBuffer sb = StringBuffer();
    for (var result in results) {
      if (result.errorCode != 0) {
        sb.write('Error ${result.errorCode}: ${result.errorMsg}\n');
        continue;
      }
      sb
        ..write('Format: ${result.format}\n')
        ..write('Text: ${result.text}\n\n');
    }
    return sb.toString().trim();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(_title)),
      body: SafeArea(
        child: Column(
          children: [
            // The image area keeps a fixed size regardless of how many
            // results there are; the results list below scrolls instead of
            // squeezing the image.
            SizedBox(
              width: double.infinity,
              height: MediaQuery.sizeOf(context).height * 0.55,
              child: FutureBuilder<_LoadedImage>(
                future: _loadFuture,
                builder: (context, snapshot) {
                  if (snapshot.connectionState != ConnectionState.done) {
                    return const Center(child: CircularProgressIndicator());
                  }
                  if (snapshot.hasError) {
                    return Center(
                        child: Text('Failed to load image: ${snapshot.error}'));
                  }

                  final _LoadedImage loaded = snapshot.data!;
                  // The SizedBox uses the natural image size, so the painter
                  // works in image pixel coordinates and FittedBox scales
                  // image and overlay together.
                  return Center(
                    child: FittedBox(
                      fit: BoxFit.contain,
                      child: SizedBox(
                        width: loaded.image.width.toDouble(),
                        height: loaded.image.height.toDouble(),
                        child: Stack(
                          children: [
                            Image.memory(loaded.bytes, fit: BoxFit.fill),
                            Positioned.fill(
                              child: CustomPaint(
                                painter: ImageResultPainter(widget.results),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
            Expanded(
              child: Container(
                width: double.infinity,
                color: Theme.of(context).colorScheme.surfaceContainerHighest,
                child: SingleChildScrollView(
                  padding: const EdgeInsets.all(16),
                  child: SelectableText(
                    _summary,
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// Draws annotation overlays (quadrilateral + text) for barcodes found in a
/// camera frame. Points are mapped from source-frame coordinates to the
/// displayed (possibly rotated) canvas.
class ResultPainter extends CustomPainter {
  final List<BarcodeResult> results;
  final int srcWidth;
  final int srcHeight;
  final double drawWidth;
  final double drawHeight;
  final int rotation;

  ResultPainter(
    this.results, {
    required this.srcWidth,
    required this.srcHeight,
    required this.drawWidth,
    required this.drawHeight,
    required this.rotation,
  });

  /// Maps a point from source-frame coordinates to the displayed (possibly
  /// rotated) canvas, so overlay boxes align with the upright preview even
  /// when the texture is rotated by [rotation] degrees clockwise.
  Offset _transform(double x, double y) {
    final int q = (rotation ~/ 90) % 4;
    switch (q) {
      case 1: // 90° clockwise
        final double nx = srcHeight - y;
        final double ny = x;
        return Offset(nx * drawWidth / srcHeight, ny * drawHeight / srcWidth);
      case 2: // 180°
        final double nx = srcWidth - x;
        final double ny = srcHeight - y;
        return Offset(nx * drawWidth / srcWidth, ny * drawHeight / srcHeight);
      case 3: // 270° clockwise (90° counter-clockwise)
        final double nx = y;
        final double ny = srcWidth - x;
        return Offset(nx * drawWidth / srcHeight, ny * drawHeight / srcWidth);
      default: // 0°
        return Offset(x * drawWidth / srcWidth, y * drawHeight / srcHeight);
    }
  }

  @override
  void paint(Canvas canvas, Size size) {
    if (results.isEmpty) return;

    final Paint boxPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    for (var result in results) {
      final Offset a = _transform(result.x1.toDouble(), result.y1.toDouble());
      final Offset b = _transform(result.x2.toDouble(), result.y2.toDouble());
      final Offset c = _transform(result.x3.toDouble(), result.y3.toDouble());
      final Offset d = _transform(result.x4.toDouble(), result.y4.toDouble());

      final Path path = Path()
        ..moveTo(a.dx, a.dy)
        ..lineTo(b.dx, b.dy)
        ..lineTo(c.dx, c.dy)
        ..lineTo(d.dx, d.dy)
        ..close();

      canvas.drawPath(path, boxPaint);

      final TextPainter textPainter = TextPainter(
        text: TextSpan(
          text: result.text,
          style: const TextStyle(
            color: Colors.red,
            fontSize: 16,
          ),
        ),
        textDirection: TextDirection.ltr,
      );

      textPainter.layout();
      textPainter.paint(canvas, a);
    }
  }

  @override
  bool shouldRepaint(covariant ResultPainter oldDelegate) =>
      results != oldDelegate.results;
}

/// Draws annotation overlays for barcodes found in a loaded image file.
/// All coordinates are image pixel coordinates (1:1 with the canvas).
class ImageResultPainter extends CustomPainter {
  final List<BarcodeResult> results;

  ImageResultPainter(this.results);

  @override
  void paint(Canvas canvas, Size size) {
    final double scale = size.width / 1000;
    final double strokeWidth = (2 * scale).clamp(1.0, 8.0);
    final double fontSize = (24 * scale).clamp(12.0, 48.0);

    final Paint boxPaint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth;

    for (var result in results) {
      if (result.errorCode != 0) continue;

      double minX = result.x1.toDouble();
      double minY = result.y1.toDouble();
      for (final (int x, int y) in <(int, int)>[
        (result.x2, result.y2),
        (result.x3, result.y3),
        (result.x4, result.y4),
      ]) {
        if (x < minX) minX = x.toDouble();
        if (y < minY) minY = y.toDouble();
      }

      final Path path = Path()
        ..moveTo(result.x1.toDouble(), result.y1.toDouble())
        ..lineTo(result.x2.toDouble(), result.y2.toDouble())
        ..lineTo(result.x3.toDouble(), result.y3.toDouble())
        ..lineTo(result.x4.toDouble(), result.y4.toDouble())
        ..close();

      canvas.drawPath(path, boxPaint);

      final TextPainter textPainter = TextPainter(
        text: TextSpan(
          text: result.text,
          style: TextStyle(
            color: Colors.blue,
            fontSize: fontSize,
            backgroundColor: Colors.white.withValues(alpha: 0.7),
          ),
        ),
        textDirection: TextDirection.ltr,
      );
      textPainter.layout(maxWidth: size.width);
      textPainter.paint(
        canvas,
        Offset(minX, (minY - fontSize).clamp(0.0, size.height)),
      );
    }
  }

  @override
  bool shouldRepaint(covariant ImageResultPainter oldDelegate) =>
      results != oldDelegate.results;
}