rf88_plugin 0.2.0
rf88_plugin: ^0.2.0 copied to clipboard
A Flutter plugin for the PointMobile RF88 RFID pistol-grip scanner module. Supports single-shot and trigger-based RFID tag reading via UART on Android. Works with PointMobile PM84 and other RF88-compa [...]
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rf88_plugin/rf88_plugin.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RF88 Scanner Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const ScannerPage(),
);
}
}
class ScannerPage extends StatefulWidget {
const ScannerPage({super.key});
@override
State<ScannerPage> createState() => _ScannerPageState();
}
class _ScannerPageState extends State<ScannerPage> {
final _rf88 = Rf88Plugin();
StreamSubscription<String>? _tagSubscription;
final List<String> _tags = [];
bool _initialized = false;
bool _connected = false;
bool _isScanning = false;
String _statusMessage = 'Not initialized';
// ──────────────────────────────────────────────
// Lifecycle
// ──────────────────────────────────────────────
@override
void initState() {
super.initState();
_initialize();
}
@override
void dispose() {
_tagSubscription?.cancel();
_rf88.stopSearch();
_rf88.close();
_rf88.deinitScanner();
super.dispose();
}
// ──────────────────────────────────────────────
// Plugin control
// ──────────────────────────────────────────────
Future<void> _initialize() async {
try {
final ok = await _rf88.initScanner();
setState(() {
_initialized = ok;
_statusMessage = ok ? 'Initialized — tap Connect' : 'Init failed (PointMobile device required)';
});
// Subscribe to the tag stream once
_tagSubscription = _rf88.onTagsRead.listen(_onTagRead);
} catch (e) {
setState(() => _statusMessage = 'Error: $e');
}
}
Future<void> _connect() async {
setState(() => _statusMessage = 'Connecting…');
await _rf88.open();
// Connection is async — status updates via logcat / connection listener.
// We optimistically mark as connected; adjust if you expose a state stream.
setState(() {
_connected = true;
_statusMessage = 'Connected';
});
}
Future<void> _disconnect() async {
await _rf88.stopSearch();
await _rf88.close();
setState(() {
_connected = false;
_isScanning = false;
_statusMessage = 'Disconnected';
});
}
Future<void> _startScan() async {
if (!_connected) return;
setState(() {
_isScanning = true;
_statusMessage = 'Scanning…';
});
await _rf88.singleSearch();
// Result arrives via _onTagRead; plugin auto-stops after first read or 2.5s
}
Future<void> _stopScan() async {
await _rf88.stopSearch();
setState(() {
_isScanning = false;
_statusMessage = 'Scan stopped';
});
}
void _onTagRead(String tag) {
if (!mounted) return;
setState(() {
_tags.insert(0, tag);
_isScanning = false;
_statusMessage = 'Tag read ✓';
});
}
void _clearTags() => setState(() => _tags.clear());
// ──────────────────────────────────────────────
// UI
// ──────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('RF88 RFID Scanner'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Clear tags',
onPressed: _tags.isEmpty ? null : _clearTags,
),
],
),
body: Column(
children: [
// Status banner
_StatusBanner(message: _statusMessage, connected: _connected),
// Control buttons
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: _initialized && !_connected ? _connect : null,
icon: const Icon(Icons.link),
label: const Text('Connect'),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.tonal(
onPressed: _connected ? _disconnect : null,
child: const Text('Disconnect'),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _connected
? (_isScanning ? _stopScan : _startScan)
: null,
icon: Icon(_isScanning ? Icons.stop : Icons.sensors),
label: Text(_isScanning ? 'Stop Scan' : 'Scan Tag'),
style: _isScanning
? FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
)
: null,
),
),
),
const SizedBox(height: 8),
const Divider(),
// Tag list
Expanded(
child: _tags.isEmpty
? const Center(
child: Text(
'No tags read yet.\nPress "Scan Tag" or use the pistol trigger.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
)
: ListView.separated(
padding: const EdgeInsets.all(8),
itemCount: _tags.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(
child: Text('${_tags.length - index}'),
),
title: Text(
_tags[index],
style: const TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
subtitle: Text('Scan #${_tags.length - index}'),
),
),
),
],
),
);
}
}
class _StatusBanner extends StatelessWidget {
const _StatusBanner({required this.message, required this.connected});
final String message;
final bool connected;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: connected
? Colors.green.withValues(alpha: 0.1)
: Colors.orange.withValues(alpha: 0.1),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
Icon(
connected ? Icons.check_circle : Icons.info_outline,
size: 18,
color: connected ? Colors.green : Colors.orange,
),
const SizedBox(width: 8),
Expanded(
child: Text(
message,
style: TextStyle(
color: connected ? Colors.green[700] : Colors.orange[800],
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
}