Skip to main content

universal_barcode_scanner 2.3.0 copy "universal_barcode_scanner: ^2.3.0" to clipboard
universal_barcode_scanner: ^2.3.0 copied to clipboard

Barcode and QR code scanner for Flutter, on Android, iOS, Linux, macOS, web and Windows, from one widget and one callback.

example/lib/main.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:universal_barcode_scanner/universal_barcode_scanner.dart';

import 'check_in.dart';

void main() => runApp(const ExampleApp());

const Color _paper = Color(0xFFF6F1EA);
const Color _white = Color(0xFFFFFDFB);
const Color _ink = Color(0xFF1D1712);
const Color _line = Color(0xFFE6DDD1);
const Color _dim = Color(0xFF6F665E);
const Color _accent = Color(0xFF3D9970);
const Color _accentInk = Color(0xFF2B7A55);
const Color _red = Color(0xFFD43F45);

/// The soft shadow under every card.
const List<BoxShadow> _shadow = <BoxShadow>[
  BoxShadow(color: Color(0x1F3B2A1A), blurRadius: 24, offset: Offset(0, 10)),
];

/// Width of the page's column of content.
const double _contentWidth = 520;

const ScannerBar _appBar = ScannerBar(
  title: 'Point at a barcode',
  centerTitle: false,
  showBackButton: true,
  backIcon: Icon(Icons.arrow_back_ios),
);

/// The three ways to use the scanner: one shot, continuous, embedded.
class ExampleApp extends StatelessWidget {
  /// Creates the example app.
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Universal Barcode Scanner',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        scaffoldBackgroundColor: _paper,
        colorScheme: ColorScheme.fromSeed(seedColor: _accent),
        chipTheme: const ChipThemeData(
          backgroundColor: _white,
          selectedColor: Color(0xFFDDEFE5),
          side: BorderSide(color: _line),
        ),
      ),
      home: const HomePage(),
    );
  }
}

/// Menu of the three modes, showing what each one returns.
class HomePage extends StatefulWidget {
  /// Creates the menu.
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  StreamSubscription<ScanResult>? _stream;

  /// What the last read gave: one code from the camera, every code of an
  /// image.
  List<ScanResult> _results = const <ScanResult>[];
  String _mode = '';
  int _count = 0;

  /// Whether the camera sits in the result tile.
  bool _embedded = false;

  /// Where the scanner's own buttons sit, in every mode.
  Alignment _buttonsAt = Alignment.centerRight;

  /// The buttons over the camera: the torch, pausing and both flips. Pausing
  /// means little to a scan that ends on its first code.
  static const Set<ScannerButton> _allButtons = <ScannerButton>{
    ScannerButton.torch,
    ScannerButton.pause,
    ScannerButton.flipHorizontal,
    ScannerButton.flipVertical,
    ScannerButton.zoom,
    ScannerButton.switchCamera,
  };
  static const Set<ScannerButton> _onceButtons = <ScannerButton>{
    ScannerButton.torch,
    ScannerButton.flipHorizontal,
    ScannerButton.flipVertical,
    ScannerButton.zoom,
    ScannerButton.switchCamera,
  };

  /// The words of the buttons and of the page, in every mode.
  ScannerLabels _labels = ScannerLabels.english;

  /// How the buttons look, in every mode.
  ScannerButtonStyle _style = const ScannerButtonStyle();

  /// Which codes the camera modes take; the others are refused over the
  /// camera and reading goes on.
  ScanValidator? _accept;

  static final List<(String, ScanValidator?)> _accepts =
      <(String, ScanValidator?)>[
    ('Any code', null),
    ('Links only', (ScanResult code) => code.content is UrlContent),
    (
      'Products only',
      (ScanResult code) =>
          code.format == BarcodeFormat.ean13 ||
          code.format == BarcodeFormat.ean8 ||
          code.format == BarcodeFormat.upcA ||
          code.format == BarcodeFormat.upcE,
    ),
  ];

  static const List<(String, ScannerButtonStyle)> _styles =
      <(String, ScannerButtonStyle)>[
    ('Round', ScannerButtonStyle()),
    (
      'Square',
      ScannerButtonStyle(
        backgroundColor: Color(0xCCFFFFFF),
        foregroundColor: Color(0xFF0E1014),
        activeBackgroundColor: _accent,
        activeForegroundColor: Color(0xFFFFFFFF),
        focusColor: _accent,
        borderRadius: BorderRadius.all(Radius.circular(10)),
      ),
    ),
    ('Large', ScannerButtonStyle(size: 56, iconSize: 26, spacing: 10)),
  ];

  @override
  void dispose() {
    _stream?.cancel();
    super.dispose();
  }

  void _found(ScanResult result, String mode) {
    if (!mounted) return;
    setState(() {
      _results = <ScanResult>[result];
      _mode = mode;
      _count++;
    });
  }

  /// An image the user picks: a photo, a screenshot, a scan. The package
  /// takes bytes; where they come from is the app's business, here
  /// `file_picker`.
  Future<void> _pickImage() async {
    final PlatformFile? file = await FilePicker.pickFile(type: FileType.image);
    if (file == null) return;
    await _readImage(await file.readAsBytes());
  }

  /// Pictures that ship with the app, one per kind of content, to try the
  /// reader without an image of one's own.
  static const List<(String, String)> _samples = <(String, String)>[
    ('Wi-Fi', 'wifi'),
    ('Link', 'link'),
    ('Contact', 'contact'),
    ('E-mail', 'email'),
    ('Phone', 'phone'),
    ('Text message', 'sms'),
    ('Place', 'place'),
    ('Event', 'event'),
    ('Product', 'product'),
    ('Two at once', 'two_codes'),
  ];

  /// The sample picture [name], from the app's assets.
  Future<void> _readSample(String name) async {
    final ByteData image = await rootBundle.load('assets/samples/$name.png');
    await _readImage(
      image.buffer.asUint8List(image.offsetInBytes, image.lengthInBytes),
    );
  }

  /// Every code in [bytes], an encoded image.
  Future<void> _readImage(Uint8List bytes) async {
    try {
      final List<ScanResult> results = await UniversalBarcodeScanner.scanImage(
        bytes,
      );
      if (!mounted) return;
      setState(() {
        _results = results;
        _mode = 'image';
        _count++;
      });
    } on ScannerException catch (error) {
      _failed(error);
    }
  }

  /// One camera at a time: the tile's goes before a scanner opens.
  Future<void> _closeEmbedded() async {
    if (!_embedded) return;
    _toggleEmbedded();
    await WidgetsBinding.instance.endOfFrame;
  }

  /// Embedded: the camera opens in the result tile, and closes from the same
  /// button.
  void _toggleEmbedded() => setState(() => _embedded = !_embedded);

  /// One shot: opens the scanner, comes back with a code or null.
  Future<void> _scanOnce() async {
    await _stream?.cancel();
    await _closeEmbedded();
    if (!mounted) return;
    try {
      final ScanResult? result = await UniversalBarcodeScanner.scanResult(
        context,
        bar: _appBar,
        cameraFace: CameraFace.back,
        scanFormat: ScanFormat.all,
        buttons: _onceButtons,
        buttonsAlignment: _buttonsAt,
        labels: _labels,
        buttonStyle: _style,
        vibrate: true,
        beep: true,
        validator: _accept,
      );
      if (result != null) _found(result, 'one shot');
    } on ScannerException catch (error) {
      _failed(error);
    }
  }

  /// Continuous: the stream closes on its own when the route goes away.
  Future<void> _scanStream() async {
    await _stream?.cancel();
    await _closeEmbedded();
    if (!mounted) return;
    _stream = UniversalBarcodeScanner.resultStream(
      context,
      bar: _appBar,
      scanDelay: const Duration(seconds: 2),
      buttons: _allButtons,
      buttonsAlignment: _buttonsAt,
      labels: _labels,
      buttonStyle: _style,
      vibrate: true,
      beep: true,
      validator: _accept,
    ).listen(
      (ScanResult result) => _found(result, 'continuous'),
      onError: (Object error) {
        if (error is ScannerException) _failed(error);
      },
    );
  }

  void _failed(ScannerException error) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(switch (error.code) {
          ScannerErrorCode.permissionDenied =>
            'The camera permission was refused.',
          ScannerErrorCode.invalidImage => 'The image could not be opened.',
          _ => 'The camera could not be used.',
        }),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        // The whole page scrolls, its scrollbar at the window's edge; the
        // content keeps to a column in the middle.
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            final double side = math.max(
              20,
              (constraints.maxWidth - _contentWidth) / 2,
            );
            return ListView(
              padding: EdgeInsets.fromLTRB(side, 28, side, 32),
              children: <Widget>[
                const Text(
                  'FLUTTER · SIX PLATFORMS',
                  style: TextStyle(
                    fontSize: 12,
                    fontWeight: FontWeight.w700,
                    letterSpacing: 2.6,
                    color: _accentInk,
                  ),
                ),
                const SizedBox(height: 8),
                const Text(
                  'Universal Barcode Scanner',
                  style: TextStyle(
                    fontSize: 30,
                    fontWeight: FontWeight.w800,
                    letterSpacing: -0.9,
                    color: _ink,
                  ),
                ),
                const SizedBox(height: 6),
                const Text(
                  'Barcodes and QR codes from one call, on Android, iOS, '
                  'Linux, macOS, web and Windows.',
                  style: TextStyle(fontSize: 15, color: _dim, height: 1.45),
                ),
                const SizedBox(height: 22),
                _Result(
                  results: _results,
                  // The camera modes never hand a refused code on; an
                  // image gives every code, and a code read before the
                  // Accept row changed is checked again.
                  accept: _accept,
                  mode: _mode,
                  count: _count,
                  camera: _embedded
                      ? UniversalBarcodeScanner(
                          continuous: true,
                          buttons: _allButtons,
                          buttonsAlignment: _buttonsAt,
                          labels: _labels,
                          buttonStyle: _style,
                          vibrate: true,
                          beep: true,
                          validator: _accept,
                          onResult: (ScanResult result) =>
                              _found(result, 'embedded'),
                          onError: _failed,
                          onCreated: (ScannerController _) {},
                        )
                      : null,
                  controls: _embedded
                      ? <Widget>[
                          OutlinedButton.icon(
                            onPressed: _toggleEmbedded,
                            icon: const Icon(Icons.close, size: 18),
                            label: const Text('Close'),
                          ),
                        ]
                      : const <Widget>[],
                ),
                const SizedBox(height: 12),
                _Options(
                  label: 'Buttons',
                  children: <Widget>[
                    for (final (String label, Alignment at)
                        in const <(String, Alignment)>[
                      ('Right side', Alignment.centerRight),
                      ('Top right', Alignment.topRight),
                      ('Top left', Alignment.topLeft),
                      ('Bottom', Alignment.bottomCenter),
                    ])
                      ChoiceChip(
                        label: Text(label),
                        selected: _buttonsAt == at,
                        onSelected: (bool _) => setState(() => _buttonsAt = at),
                      ),
                  ],
                ),
                const SizedBox(height: 8),
                _Options(
                  label: 'Look',
                  children: <Widget>[
                    for (final (String label, ScannerButtonStyle style)
                        in _styles)
                      ChoiceChip(
                        label: Text(label),
                        selected: _style == style,
                        onSelected: (bool _) => setState(() => _style = style),
                      ),
                  ],
                ),
                const SizedBox(height: 8),
                _Options(
                  label: 'Words',
                  children: <Widget>[
                    for (final (String label, ScannerLabels labels)
                        in const <(String, ScannerLabels)>[
                      ('English', ScannerLabels.english),
                      ('Français', ScannerLabels.french),
                      ('Nederlands', ScannerLabels.dutch),
                      ('Deutsch', ScannerLabels.german),
                    ])
                      ChoiceChip(
                        label: Text(label),
                        selected: identical(_labels, labels),
                        onSelected: (bool _) =>
                            setState(() => _labels = labels),
                      ),
                  ],
                ),
                const SizedBox(height: 8),
                _Options(
                  label: 'Accept',
                  children: <Widget>[
                    for (final (String label, ScanValidator? accept)
                        in _accepts)
                      ChoiceChip(
                        label: Text(label),
                        selected: identical(_accept, accept),
                        onSelected: (bool _) =>
                            setState(() => _accept = accept),
                      ),
                  ],
                ),
                const SizedBox(height: 22),
                _Mode(
                  title: 'Embedded view',
                  body: 'Puts the camera inside your own layout, here the tile '
                      'above, with buttons for the torch, pausing and '
                      'flipping.',
                  action: 'Open',
                  // Closed from the tile, next to the other controls.
                  onPressed: _embedded ? null : _toggleEmbedded,
                ),
                _Mode(
                  title: 'Scan once',
                  body:
                      'Opens the scanner, closes on the first code and returns '
                      'it. A Future<String?>, null if the user backs out.',
                  action: 'Scan',
                  onPressed: _scanOnce,
                ),
                _Mode(
                  title: 'Scan continuously',
                  body: 'Stays open and reports every code as it comes, two '
                      'seconds apart. A Stream<String> that closes with the '
                      'route.',
                  action: 'Open',
                  onPressed: _scanStream,
                ),
                _Mode(
                  title: 'Read an image',
                  body: 'Reads every code in a picture, says what each one '
                      'holds and which ones the Accept row refuses. Pick one '
                      'of yours, or try one of these:',
                  action: 'Pick',
                  onPressed: _pickImage,
                  footer: Wrap(
                    spacing: 6,
                    runSpacing: 6,
                    children: <Widget>[
                      for (final (String label, String name) in _samples)
                        ActionChip(
                          label: Text(label),
                          visualDensity: VisualDensity.compact,
                          onPressed: () => _readSample(name),
                        ),
                    ],
                  ),
                ),
                const SizedBox(height: 10),
                const _ShowcaseEntry(),
                const SizedBox(height: 16),
                const _Note(),
              ],
            );
          },
        ),
      ),
    );
  }
}

/// The way into [CheckInDemo]: the package at work in an app of its own.
class _ShowcaseEntry extends StatelessWidget {
  const _ShowcaseEntry();

  @override
  Widget build(BuildContext context) {
    return Material(
      borderRadius: BorderRadius.circular(18),
      clipBehavior: Clip.antiAlias,
      child: Ink(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: <Color>[Color(0xFF2B7A55), Color(0xFF3D9970)],
          ),
        ),
        child: InkWell(
          onTap: () => Navigator.of(context).push(
            MaterialPageRoute<void>(
              builder: (BuildContext context) => const CheckInDemo(),
            ),
          ),
          child: const Padding(
            padding: EdgeInsets.fromLTRB(20, 18, 16, 18),
            child: Row(
              children: <Widget>[
                Icon(Icons.confirmation_number_outlined,
                    size: 34, color: Colors.white),
                SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        'Event check-in',
                        style: TextStyle(
                          fontSize: 17,
                          fontWeight: FontWeight.w700,
                          color: Colors.white,
                        ),
                      ),
                      SizedBox(height: 3),
                      Text(
                        'A gate app on the embedded view: tickets read, guests '
                        'counted, the wrong ones refused.',
                        style: TextStyle(
                          fontSize: 13.5,
                          height: 1.4,
                          color: Color(0xE6FFFFFF),
                        ),
                      ),
                    ],
                  ),
                ),
                SizedBox(width: 10),
                Icon(Icons.arrow_forward, color: Colors.white),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

/// What the last scan returned, or what to do to get one.
class _Result extends StatelessWidget {
  const _Result({
    required this.results,
    required this.accept,
    required this.mode,
    required this.count,
    this.camera,
    this.controls = const <Widget>[],
  });

  /// The codes of the last read: one from the camera, any number from an
  /// image.
  final List<ScanResult> results;

  /// The Accept row's choice, which [results] are checked against.
  final ScanValidator? accept;
  final String mode;
  final int count;

  /// The embedded scanner, when it is open in this tile.
  final Widget? camera;

  /// What drives [camera].
  final List<Widget> controls;

  @override
  Widget build(BuildContext context) {
    final bool found = results.isNotEmpty;
    final ScanValidator? accept = this.accept;
    final List<bool> refused = <bool>[
      for (final ScanResult result in results)
        accept != null && !accept(result),
    ];
    final bool emptyImage = !found && mode == 'image' && count > 0;
    final bool allRefused = found && refused.every((bool no) => no);
    final Color tone = allRefused ? _red : _accent;
    final Widget? camera = this.camera;
    final String heading = !found
        ? emptyImage
            ? 'NO CODE IN THE IMAGE'
            : 'NOTHING SCANNED YET'
        : allRefused
            ? results.length > 1
                ? 'REFUSED, $mode, ${results.length} CODES'
                : 'REFUSED, $mode'
            : results.length > 1
                ? 'SCANNED, $mode, ${results.length} CODES'
                : results.single.format == BarcodeFormat.unknown
                    ? 'SCANNED, $mode'
                    : 'SCANNED, $mode, ${_formatLabel(results.single.format)}';

    return Container(
      width: double.infinity,
      padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
      decoration: BoxDecoration(
        color: !found
            ? _white
            : allRefused
                ? const Color(0xFFFCEDEC)
                : const Color(0xFFEAF5EF),
        border: Border.all(color: found ? tone : _line),
        borderRadius: BorderRadius.circular(16),
        boxShadow: _shadow,
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Row(
            children: <Widget>[
              Icon(
                !found
                    ? Icons.qr_code_scanner
                    : allRefused
                        ? Icons.block
                        : Icons.check_circle,
                size: 17,
                color: found ? tone : _dim,
              ),
              const SizedBox(width: 8),
              Text(
                heading,
                style: TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w700,
                  letterSpacing: 1.2,
                  color: found ? (allRefused ? _red : _accentInk) : _dim,
                ),
              ),
              const Spacer(),
              if (count > 1)
                Text(
                  '$count reads',
                  style: const TextStyle(fontSize: 12, color: _dim),
                ),
            ],
          ),
          if (camera != null) ...<Widget>[
            const SizedBox(height: 14),
            ClipRRect(
              borderRadius: BorderRadius.circular(10),
              child: SizedBox(
                height: 210,
                width: double.infinity,
                child: camera,
              ),
            ),
          ],
          if (!found) ...<Widget>[
            const SizedBox(height: 12),
            Text(
              emptyImage
                  ? 'The image holds no code this platform reads.'
                  : camera == null
                      ? 'Pick a mode below and point the camera at a barcode.'
                      : 'Point the camera at a barcode.',
              style: const TextStyle(fontSize: 14.5, height: 1.4, color: _dim),
            ),
          ],
          for (final (int i, ScanResult result) in results.indexed)
            _Code(
              result,
              withFormat: results.length > 1,
              refused: refused[i],
            ),
          if (controls.isNotEmpty) ...<Widget>[
            const SizedBox(height: 14),
            // Compact, so the three fit on one line of a phone.
            OutlinedButtonTheme(
              data: OutlinedButtonThemeData(
                style: OutlinedButton.styleFrom(
                  visualDensity: VisualDensity.compact,
                  padding: const EdgeInsets.symmetric(horizontal: 10),
                  // Neutral: closing is not the tile's green action.
                  foregroundColor: _ink,
                  side: const BorderSide(color: _line),
                ),
              ),
              child: Wrap(spacing: 6, runSpacing: 8, children: controls),
            ),
          ],
        ],
      ),
    );
  }
}

/// One code read: its text, and what it holds where the text follows a
/// format a phone knows.
class _Code extends StatelessWidget {
  const _Code(
    this.result, {
    required this.withFormat,
    required this.refused,
  });

  final ScanResult result;

  /// Whether the Accept row refused it.
  final bool refused;

  /// Whether to name the symbology, when several codes share the tile.
  final bool withFormat;

  @override
  Widget build(BuildContext context) {
    final String? meaning = _describe(result.content);
    final String format = _formatLabel(result.format);
    return Padding(
      padding: const EdgeInsets.only(top: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          SelectableText(
            result.text,
            style: const TextStyle(
              fontSize: 17,
              height: 1.4,
              fontFamily: 'monospace',
              fontWeight: FontWeight.w600,
              color: _ink,
            ),
          ),
          const SizedBox(height: 4),
          Text(
            <String>[
              if (withFormat && format.isNotEmpty) format,
              '${result.text.length} characters',
            ].join(' · '),
            style: const TextStyle(fontSize: 12.5, color: _dim),
          ),
          if (meaning != null) ...<Widget>[
            const SizedBox(height: 6),
            Row(
              children: <Widget>[
                const Icon(Icons.auto_awesome, size: 15, color: _accentInk),
                const SizedBox(width: 6),
                Expanded(
                  child: Text(
                    meaning,
                    style: const TextStyle(fontSize: 13.5, color: _accentInk),
                  ),
                ),
              ],
            ),
          ],
          if (refused) ...<Widget>[
            const SizedBox(height: 6),
            const Row(
              children: <Widget>[
                Icon(Icons.block, size: 15, color: _red),
                SizedBox(width: 6),
                Expanded(
                  child: Text(
                    'Refused by the Accept row: a scanner would say so and '
                    'read on.',
                    style: TextStyle(fontSize: 13.5, color: _red),
                  ),
                ),
              ],
            ),
          ],
        ],
      ),
    );
  }
}

/// What a code holds, in words, or null for plain text.
String? _describe(ScanContent? content) => switch (content) {
      UrlContent(:final Uri url) => 'A link to ${url.host}',
      WifiContent(:final String ssid, :final WifiSecurity security) =>
        'A Wi-Fi network, $ssid, '
            '${security == WifiSecurity.open ? 'open' : security.name.toUpperCase()}',
      ContactContent(:final String? name, :final String? organization) =>
        'A contact, ${name ?? organization ?? 'with no name'}',
      EmailContent(:final String address) => 'An e-mail to $address',
      PhoneContent(:final String number) => 'A phone number, $number',
      SmsContent(:final String number) => 'A text message to $number',
      GeoContent(:final double latitude, :final double longitude) =>
        'A place, $latitude, $longitude',
      EventContent(:final String? summary) =>
        'An event, ${summary ?? 'untitled'}',
      null => null,
    };

/// A row of choices after its label. The labels share one width, so every
/// row of choices starts at the same place.
class _Options extends StatelessWidget {
  const _Options({required this.label, required this.children});

  final String label;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        SizedBox(
          width: 70,
          // Level with the text of the first line of chips.
          child: Padding(
            padding: const EdgeInsets.only(top: 9),
            child: Text(
              label,
              style: const TextStyle(fontSize: 13, color: _dim),
            ),
          ),
        ),
        Expanded(
          child: Wrap(spacing: 8, runSpacing: 8, children: children),
        ),
      ],
    );
  }
}

/// One of the ways in, with what it gives back.
class _Mode extends StatelessWidget {
  const _Mode({
    required this.title,
    required this.body,
    required this.action,
    required this.onPressed,
    this.footer,
  });

  final String title;
  final String body;
  final String action;
  final VoidCallback? onPressed;

  /// Under the text, across the card.
  final Widget? footer;

  @override
  Widget build(BuildContext context) {
    final bool off = onPressed == null;

    return Container(
      margin: const EdgeInsets.only(bottom: 12),
      padding: const EdgeInsets.fromLTRB(20, 16, 16, 16),
      decoration: BoxDecoration(
        color: _white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: _shadow,
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      title,
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.w600,
                        color: off ? _dim : _ink,
                      ),
                    ),
                    const SizedBox(height: 5),
                    Text(
                      body,
                      style: const TextStyle(
                        fontSize: 13.5,
                        color: _dim,
                        height: 1.45,
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(width: 14),
              FilledButton(
                onPressed: onPressed,
                style: FilledButton.styleFrom(
                  backgroundColor: _accent,
                  foregroundColor: Colors.white,
                  disabledBackgroundColor: _line,
                  disabledForegroundColor: _dim,
                  padding: const EdgeInsets.symmetric(horizontal: 20),
                ),
                child: Text(action),
              ),
            ],
          ),
          if (footer case final Widget footer) ...<Widget>[
            const SizedBox(height: 12),
            footer,
          ],
        ],
      ),
    );
  }
}

/// What a browser needs before any of this works.
class _Note extends StatelessWidget {
  const _Note();

  @override
  Widget build(BuildContext context) {
    if (!kIsWeb) return const SizedBox.shrink();

    return const Padding(
      padding: EdgeInsets.symmetric(horizontal: 4),
      child: Text(
        'In a browser the camera needs your permission, and it is only offered '
        'over HTTPS. A machine without one says so rather than showing you a '
        'black screen.',
        style: TextStyle(fontSize: 12.5, color: _dim, height: 1.5),
      ),
    );
  }
}

/// How a symbology is usually written.
String _formatLabel(BarcodeFormat format) => switch (format) {
      BarcodeFormat.aztec => 'Aztec',
      BarcodeFormat.codabar => 'Codabar',
      BarcodeFormat.code39 => 'Code 39',
      BarcodeFormat.code93 => 'Code 93',
      BarcodeFormat.code128 => 'Code 128',
      BarcodeFormat.dataMatrix => 'Data Matrix',
      BarcodeFormat.ean8 => 'EAN-8',
      BarcodeFormat.ean13 => 'EAN-13',
      BarcodeFormat.itf => 'ITF',
      BarcodeFormat.pdf417 => 'PDF417',
      BarcodeFormat.qrCode => 'QR code',
      BarcodeFormat.upcA => 'UPC-A',
      BarcodeFormat.upcE => 'UPC-E',
      BarcodeFormat.unknown => '',
    };
1
likes
160
points
1.33k
downloads
screenshot

Documentation

API reference

Publisher

verified publishercomapps.be

Weekly Downloads

Barcode and QR code scanner for Flutter, on Android, iOS, Linux, macOS, web and Windows, from one widget and one callback.

Homepage
Repository (GitHub)
View/report issues

Topics

#barcode #qrcode #scanner #camera #reader

Funding

Consider supporting this project:

www.paypal.com

License

MIT (license)

Dependencies

flutter, flutter_plugin_android_lifecycle, web, webview_all

More

Packages that depend on universal_barcode_scanner

Packages that implement universal_barcode_scanner