# Scanner to API Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Build a Flutter app that captures barcode scans from the D500 PAD scanner and sends the data to a user-configured HTTP endpoint. **Architecture:** Android BroadcastReceiver captures scan broadcasts from the device's built-in scanning service, forwards data to Flutter via EventChannel. The Dart side receives scan events, sends HTTP POST to the configured URL, and displays results. **Tech Stack:** Flutter 3.x, Dart, Android native (Kotlin), http package, shared_preferences --- ### Task 1: Add Dependencies **Files:** - Modify: `pubspec.yaml` **Step 1: Add http and shared_preferences to pubspec.yaml** Add under `dependencies:` (after `cupertino_icons`): ```yaml http: ^1.2.0 shared_preferences: ^2.2.0 ``` **Step 2: Run flutter pub get** Run: `flutter pub get` Expected: dependencies resolved successfully **Step 3: Commit** ```bash git add pubspec.yaml pubspec.lock git commit -m "feat: add http and shared_preferences dependencies" ``` --- ### Task 2: Create Scan Record Model **Files:** - Create: `lib/models/scan_record.dart` - Create: `test/models/scan_record_test.dart` **Step 1: Write the failing test** Create `test/models/scan_record_test.dart`: ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:pad_scanner/models/scan_record.dart'; void main() { group('ScanRecord', () { test('creates from map', () { final record = ScanRecord( barcode: '1234567890', codeType: 'CODE128', timestamp: DateTime.parse('2026-05-07T10:30:00Z'), ); expect(record.barcode, '1234567890'); expect(record.codeType, 'CODE128'); expect(record.status, SendStatus.pending); }); test('toJson produces correct map', () { final record = ScanRecord( barcode: 'ABC123', codeType: 'QR', timestamp: DateTime.parse('2026-05-07T10:30:00Z'), ); final json = record.toJson(); expect(json['barcode'], 'ABC123'); expect(json['code_type'], 'QR'); expect(json['timestamp'], '2026-05-07T10:30:00.000Z'); }); }); } ``` **Step 2: Run test to verify it fails** Run: `flutter test test/models/scan_record_test.dart` Expected: FAIL - file not found **Step 3: Write minimal implementation** Create `lib/models/scan_record.dart`: ```dart enum SendStatus { pending, success, failed } class ScanRecord { final String barcode; final String codeType; final DateTime timestamp; SendStatus status; ScanRecord({ required this.barcode, required this.codeType, required this.timestamp, this.status = SendStatus.pending, }); Map toJson() => { 'barcode': barcode, 'code_type': codeType, 'timestamp': timestamp.toUtc().toIso8601String(), }; } ``` **Step 4: Run test to verify it passes** Run: `flutter test test/models/scan_record_test.dart` Expected: PASS **Step 5: Commit** ```bash git add lib/models/scan_record.dart test/models/scan_record_test.dart git commit -m "feat: add ScanRecord model with toJson" ``` --- ### Task 3: Create API Service **Files:** - Create: `lib/services/api_service.dart` - Create: `test/services/api_service_test.dart` **Step 1: Write the failing test** Create `test/services/api_service_test.dart`: ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:pad_scanner/services/api_service.dart'; void main() { group('ApiService', () { test('sendScanData returns true on 200', () async { final client = MockClient((request) async { return http.Response('{"status":"ok"}', 200); }); final service = ApiService(client: client); final result = await service.sendScanData( 'http://localhost:8000/scan', barcode: '123456', codeType: 'CODE128', ); expect(result, isTrue); }); test('sendScanData returns false on error', () async { final client = MockClient((request) async { return http.Response('error', 500); }); final service = ApiService(client: client); final result = await service.sendScanData( 'http://localhost:8000/scan', barcode: '123456', codeType: 'CODE128', ); expect(result, isFalse); }); }); } ``` **Step 2: Run test to verify it fails** Run: `flutter test test/services/api_service_test.dart` Expected: FAIL **Step 3: Write minimal implementation** Create `lib/services/api_service.dart`: ```dart import 'dart:convert'; import 'package:http/http.dart' as http; class ApiService { final http.Client _client; ApiService({http.Client? client}) : _client = client ?? http.Client(); Future sendScanData( String url, { required String barcode, required String codeType, }) async { try { final response = await _client .post( Uri.parse(url), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'barcode': barcode, 'code_type': codeType, 'timestamp': DateTime.now().toUtc().toIso8601String(), }), ) .timeout(const Duration(seconds: 5)); return response.statusCode >= 200 && response.statusCode < 300; } catch (_) { return false; } } } ``` **Step 4: Run test to verify it passes** Run: `flutter test test/services/api_service_test.dart` Expected: PASS **Step 5: Commit** ```bash git add lib/services/api_service.dart test/services/api_service_test.dart git commit -m "feat: add ApiService for HTTP POST" ``` --- ### Task 4: Create Scanner Service (Flutter EventChannel) **Files:** - Create: `lib/services/scanner_service.dart` **Step 1: Write the scanner service** Create `lib/services/scanner_service.dart`: ```dart import 'dart:async'; import 'package:flutter/services.dart'; class ScanResult { final String barcode; final String codeType; ScanResult({required this.barcode, required this.codeType}); } class ScannerService { static const _eventChannel = EventChannel('com.example.pad_scanner/scan'); Stream? _scanStream; Stream get scanResults { _scanStream ??= _eventChannel .receiveBroadcastStream() .map((event) => event as Map) .map((event) => ScanResult( barcode: event['barcode'] as String? ?? '', codeType: event['codeType'] as String? ?? 'UNKNOWN', )); return _scanStream!; } } ``` **Step 2: Commit** ```bash git add lib/services/scanner_service.dart git commit -m "feat: add ScannerService with EventChannel" ``` --- ### Task 5: Android Native - BroadcastReceiver + EventChannel **Files:** - Modify: `android/app/src/main/kotlin/com/example/pad_scanner/MainActivity.kt` **Step 1: Implement BroadcastReceiver and EventChannel in MainActivity** Replace entire content of `MainActivity.kt`: ```kotlin package com.example.pad_scanner import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import io.flutter.embedding.android.FlutterActivity import io.flutter.plugin.common.EventChannel class MainActivity : FlutterActivity() { private var scanReceiver: BroadcastReceiver? = null override fun onResume() { super.onResume() registerScanReceiver() } override fun onPause() { super.onPause() unregisterScanReceiver() } private fun registerScanReceiver() { scanReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // noop - handled by EventChannel sink } } } private fun unregisterScanReceiver() { scanReceiver?.let { unregisterReceiver(it) scanReceiver = null } } override fun configureFlutterEngine(flutterEngine: io.flutter.embedding.engine.FlutterEngine) { super.configureFlutterEngine(flutterEngine) EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.pad_scanner/scan") .setStreamHandler(object : EventChannel.StreamHandler { private var receiver: BroadcastReceiver? = null override fun onListen(arguments: Any?, events: io.flutter.plugin.common.EventChannel.EventSink?) { if (events == null) return receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val barcode = intent.getStringExtra("scannerdata") ?: return events.success(mapOf( "barcode" to barcode, "codeType" to "UNKNOWN" )) } } val filter = IntentFilter("com.android.server.scannerservice.broadcast") registerReceiver(receiver, filter) } override fun onCancel(arguments: Any?) { receiver?.let { unregisterReceiver(it) receiver = null } } }) } } ``` **Step 2: Commit** ```bash git add android/app/src/main/kotlin/com/example/pad_scanner/MainActivity.kt git commit -m "feat: add BroadcastReceiver and EventChannel for scanner" ``` --- ### Task 6: Settings Page **Files:** - Create: `lib/pages/settings_page.dart` **Step 1: Write the settings page** Create `lib/pages/settings_page.dart`: ```dart import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:pad_scanner/services/api_service.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { final _controller = TextEditingController(); final _apiService = ApiService(); bool _saving = false; @override void initState() { super.initState(); _loadUrl(); } Future _loadUrl() async { final prefs = await SharedPreferences.getInstance(); _controller.text = prefs.getString('api_url') ?? ''; } Future _saveUrl() async { setState(() => _saving = true); final prefs = await SharedPreferences.getInstance(); await prefs.setString('api_url', _controller.text.trim()); setState(() => _saving = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('URL saved')), ); } } Future _testConnection() async { final url = _controller.text.trim(); if (url.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please enter a URL first')), ); return; } final ok = await _apiService.sendScanData( url, barcode: 'TEST_BARCODE', codeType: 'TEST', ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(ok ? 'Connection OK' : 'Connection failed')), ); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Settings')), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: _controller, decoration: const InputDecoration( labelText: 'API URL', hintText: 'http://192.168.1.100:8000/scan', border: OutlineInputBorder(), ), keyboardType: TextInputType.url, ), const SizedBox(height: 16), ElevatedButton( onPressed: _saving ? null : _saveUrl, child: _saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Save'), ), const SizedBox(height: 8), OutlinedButton( onPressed: _testConnection, child: const Text('Test Connection'), ), ], ), ), ); } } ``` **Step 2: Commit** ```bash git add lib/pages/settings_page.dart git commit -m "feat: add settings page for URL configuration" ``` --- ### Task 7: Main Scan Page **Files:** - Create: `lib/pages/scan_page.dart` **Step 1: Write the scan page** Create `lib/pages/scan_page.dart`: ```dart import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:pad_scanner/models/scan_record.dart'; import 'package:pad_scanner/services/scanner_service.dart'; import 'package:pad_scanner/services/api_service.dart'; import 'package:pad_scanner/pages/settings_page.dart'; class ScanPage extends StatefulWidget { const ScanPage({super.key}); @override State createState() => _ScanPageState(); } class _ScanPageState extends State { final _scannerService = ScannerService(); final _apiService = ApiService(); final _records = []; String _status = 'Waiting for scan...'; @override void initState() { super.initState(); _startListening(); } void _startListening() { _scannerService.scanResults.listen((result) async { final record = ScanRecord( barcode: result.barcode, codeType: result.codeType, timestamp: DateTime.now(), ); setState(() { _records.insert(0, record); _status = 'Sending...'; }); final prefs = await SharedPreferences.getInstance(); final url = prefs.getString('api_url') ?? ''; if (url.isEmpty) { setState(() { record.status = SendStatus.failed; _status = 'No URL configured. Go to Settings.'; }); return; } final ok = await _apiService.sendScanData( url, barcode: record.barcode, codeType: record.codeType, ); if (mounted) { setState(() { record.status = ok ? SendStatus.success : SendStatus.failed; _status = ok ? 'Sent successfully' : 'Send failed'; }); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('PAD Scanner'), actions: [ IconButton( icon: const Icon(Icons.settings), onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const SettingsPage()), ), ), ], ), body: Column( children: [ Container( width: double.infinity, padding: const EdgeInsets.all(16), color: Theme.of(context).colorScheme.surfaceContainerHighest, child: Text( _status, style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center, ), ), Expanded( child: _records.isEmpty ? const Center( child: Text('No scans yet.\nPress the scan button on the device.', textAlign: TextAlign.center)) : ListView.builder( itemCount: _records.length, itemBuilder: (context, index) { final r = _records[index]; return ListTile( title: Text(r.barcode), subtitle: Text( '${r.codeType} ${r.timestamp.toLocal().toIso8601String().substring(0, 19)}'), trailing: Icon( r.status == SendStatus.success ? Icons.check_circle : r.status == SendStatus.failed ? Icons.error : Icons.hourglass_empty, color: r.status == SendStatus.success ? Colors.green : r.status == SendStatus.failed ? Colors.red : Colors.orange, ), ); }, ), ), ], ), ); } } ``` **Step 2: Commit** ```bash git add lib/pages/scan_page.dart git commit -m "feat: add scan page with record list and send logic" ``` --- ### Task 8: Wire Up main.dart **Files:** - Modify: `lib/main.dart` **Step 1: Replace main.dart** Replace entire content of `lib/main.dart`: ```dart import 'package:flutter/material.dart'; import 'package:pad_scanner/pages/scan_page.dart'; void main() { runApp(const PadScannerApp()); } class PadScannerApp extends StatelessWidget { const PadScannerApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'PAD Scanner', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), home: const ScanPage(), ); } } ``` **Step 2: Commit** ```bash git add lib/main.dart git commit -m "feat: wire up main.dart with ScanPage" ``` --- ### Task 9: Update AndroidManifest **Files:** - Modify: `android/app/src/main/AndroidManifest.xml` **Step 1: Add INTERNET permission and uses-library** Add inside `` tag (before ``): ```xml ``` Add inside `` tag (before the ``): ```xml ``` **Step 2: Commit** ```bash git add android/app/src/main/AndroidManifest.xml git commit -m "feat: add INTERNET permission and scanner library" ``` --- ### Task 10: Build and Deploy to Device **Step 1: Run all unit tests** Run: `flutter test` Expected: all tests PASS **Step 2: Build and deploy to D500 device** Make sure device is connected via USB, then: Run: `flutter run -d ` **Step 3: Verify on device** 1. App launches with "Waiting for scan..." status 2. Go to Settings, enter a URL (or use a mock server), save 3. Press the scan button on the device, scan a barcode 4. Verify the barcode appears in the list 5. Verify the send status indicator updates **Step 4: Final commit (if any fixes needed)** ```bash git add -A git commit -m "fix: adjustments from device testing" ```