barcode_port 1.0.0
barcode_port: ^1.0.0 copied to clipboard
Hardware barcode input for Flutter on Android, with keyboard-wedge HID, iMin broadcasts, screen lifecycle management, and optional scan processing.
BarcodePort #
Hardware barcode input for Flutter on Android.
العربية · API · Example · Device qualification
Receive keyboard-wedge HID input and Android iMin broadcasts through one API. A focused text field is not required. Keep product lookup, inventory, and checkout rules in your application.
- A distinct event ID and UTC timestamp for every observed scan.
- Optional screen/app lifecycle management with
ScannerListener. - Opt-in normalization, filtering, and consecutive-repeat suppression.
- Typed diagnostics without barcode contents or automatic barcode logging.
- One active input owner, with asynchronous start, stop, and cleanup.

Support and requirements #
| Input | Android | Notes |
|---|---|---|
| Keyboard-wedge HID | Implemented | USB or Bluetooth readers configured to send keyboard input; Enter or Tab suffix by default. |
| iMin broadcasts | Implemented | Requires a compatible broadcast action/payload and device configuration. |
| Web HID demonstration | Example only | Exercises keyboard input; not an additional certified platform. |
See the qualification records for actual tested models and release status. An implementation is not a guarantee for every device. Camera scanning, BLE/SPP protocols, USB serial, scanner discovery, GS1 parsing, and background reception are outside the package's current scope.
Requires Dart 3.10.7+, Flutter 3.38.6+, Android API 24+, compileSdk 36, and Java/Kotlin target 17. Android is the declared plugin platform. Native iMin support is not available on web, iOS, or desktop.
Install #
flutter pub add barcode_port
Or add:
dependencies:
barcode_port: ^1.0.0
Quick start #
This complete example owns the scanner for the screen and handles input errors:
import 'package:barcode_port/scanner_widgets.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
body: ScannerListener(
createScanner: () => Scanner(),
onScan: (scan) => debugPrint('Read: ${scan.barcode}'),
onError: (error, stack) => FlutterError.reportError(
FlutterErrorDetails(exception: error, stack: stack),
),
child: const Center(child: Text('Ready to scan')),
),
),
));
}
The explicit debugPrint is demonstration application code; remove it if barcode
contents must not be logged. The package does not log barcode values itself.
ScannerListener creates one source per State and owns its start/stop/dispose.
Reception requires enabled, the current route, and a resumed app. Covering the
screen with a page/dialog or moving the app to the background pauses reception.
Set enabled from tab visibility for hidden tabs and nested navigators.
Use a new widget key to supply a different source. Finish old-owner cleanup before starting another owner; simultaneous owners fail instead of stealing reception. The source factory is not re-run when options or callbacks change during rebuild.
Direct lifecycle #
Use the headless API when your application owns the asynchronous lifetime:
import 'package:barcode_port/barcode_port.dart';
Future<void> readUntil(Future<void> finished) async {
final scanner = Scanner();
final subscription = scanner.events.listen(
(scan) => print('${scan.eventId}: ${scan.barcode}'),
onError: (Object error, StackTrace stack) {
// Report an operational error in your application.
},
);
try {
await scanner.start();
await finished;
} finally {
await subscription.cancel();
await scanner.dispose();
}
}
Subscribe before start to retain startup frames. stop waits for pending
startup and permits a later start. dispose permanently releases the source and
closes its streams. Cancel or resume paused subscriptions before awaiting disposal.
Start/stop/dispose futures can fail in addition to stream errors. Retry cleanup
when it fails; ownership is retained until resources can be released safely.
Input policy and focus #
| Option | Default | Meaning |
|---|---|---|
mode |
ScannerMode.auto |
Prefer iMin on matching host metadata; otherwise HID. |
configureIminOutput |
false |
Explicitly request broadcast output configuration on iMin. |
frameTimeout |
2 seconds | Discard an incomplete HID frame after an inter-character gap. |
terminators |
Enter, Tab | Complete the current HID frame. |
maxFrameLength |
4096 UTF-16 units | Discard an oversized HID frame through its boundary. |
passThroughEditableText |
true |
Leave keyboard input to the focused editable field. |
HID receives keyboard events; it does not identify a USB scanner or reliably distinguish human typing. Entering an editable field immediately clears a partial HID frame. iMin broadcasts are independent of text-field focus. Disabling editable pass-through does not guarantee suppression of text entry by other Flutter handlers.
Auto mode selects one transport per start. It does not switch after a start error or infer hardware failure from a quiet period. Use an explicit mode when needed.
Configuring iMin changes device output settings, and stopping does not restore previous settings. A configuration response or registered receiver is not proof that a physical scanner is ready. Treat scanned/broadcast text as application input and apply your own business validation.
Text is trimmed; blanks are discarded. Byte payloads use UTF-8. This is a text input API, not lossless arbitrary QR/binary capture. Internal codes and leading zeros are accepted. Default repeated scans remain independent events.
Optional processing #
final scanner = Scanner(
processing: ScanProcessingOptions(
normalize: (text) => text.toUpperCase(),
accept: (text) => text.startsWith('ITEM-'),
duplicateWindow: const Duration(milliseconds: 500),
),
);
Order: normalize → trim → reject empty → filter → consecutive-repeat check → deliver. Normalization preserves the event ID and UTC timestamp. Defaults do not normalize/filter/suppress repeats and pass through the same event object.
Repeat suppression is disabled by default. When enabled, only consecutive accepted equal values less than the window apart are suppressed. The exact time boundary is accepted; a suppressed read does not extend the window. A/B/A stays three accepted reads, and stop/restart resets the history. It uses monotonic time. Two intentional scans can mean two items, so choose this policy explicitly.
Callback exceptions arrive as stream errors with their stack; later reads continue.
No symbol format is guessed from text length, and no GS1 prefix is removed.
ProcessingScanner can wrap a custom BarcodeScanner and owns its disposal.
Diagnostics #
final notices = scanner.diagnostics.listen((notice) {
print('${notice.reason.name}, length=${notice.inputLength}');
});
// Cancel notices before disposing its scanner.
Reasons: incompleteFrame, frameOverflow, blankFrame, filtered,
normalizedEmpty, and duplicateSuppressed. Notices contain UTC time, input
length, and an event ID when a frame already exists; they contain no barcode text.
Operational failures remain errors on events. Overflow is diagnosed once per
frame; intentional focus/stop/dispose resets do not produce timeout notices.
Example and troubleshooting #
cd example
flutter pub get
flutter run
To explicitly configure iMin broadcast output (off by default):
flutter run --dart-define=BARCODE_PORT_CONFIGURE_IMIN_OUTPUT=true
For the optional processing demonstration:
flutter run --dart-define=SCANNER_EXAMPLE_POLICY=true
The example owns its scanner, including injected sources. Use a hot restart after changing compile-time flags. See the example guide.
| Symptom | Check |
|---|---|
| HID read never completes | Scanner keyboard mode, Enter/Tab suffix, timeout, active screen, editable focus. |
| Ordinary typing becomes a scan | HID receives keyboard input; choose screen eligibility and device configuration deliberately. |
| Duplicate-owner error | Await the old owner's stop/dispose before starting a new owner. |
| iMin sends no reads | Mode, actual broadcast configuration/action/payload, device firmware, and diagnostics/errors. |
| Repeat policy removes intentional reads | Leave duplicateWindow at zero where repeated scans represent separate actions. |
Development and support #
Contributing · Support · Security · Release checklist · Changelog
Report a device issue with its model, Android/firmware version, connection mode, scanner output settings, and a minimal example using synthetic barcode text. Use GitHub's private vulnerability reporting for security reports; do not post credentials, customer barcodes, or payment data in public issues.
The low-level native bridge is also available through
package:barcode_port/scanner_bridge.dart. Advanced/custom sources implement
BarcodeScanner and optionally DiagnosticBarcodeScanner. Generated Pigeon types
are implementation details. The package uses Flutter and Dart's meta annotations;
no manufacturer SDK is bundled. Distributed under the MIT license.