Files
minhthu/CLAUDE.md
2025-09-16 23:14:35 +07:00

12 KiB

Flutter Barcode Scanner App Guidelines

App Overview

Simple barcode scanner app with two screens:

  1. Home Screen: Barcode scanner + scan result display + history list
  2. Detail Screen: 4 text fields + Save (API call) & Print buttons

Project Structure

lib/
  core/
    constants/
    theme/
    widgets/
    network/
      api_client.dart
  features/
    scanner/
      data/
        datasources/
          scanner_remote_datasource.dart
        models/
          scan_item.dart
          save_request_model.dart
        repositories/
          scanner_repository.dart
      domain/
        entities/
          scan_entity.dart
        repositories/
          scanner_repository.dart
        usecases/
          save_scan_usecase.dart
      presentation/
        providers/
          scanner_provider.dart
        pages/
          home_page.dart
          detail_page.dart
        widgets/
          barcode_scanner_widget.dart
          scan_result_display.dart
          scan_history_list.dart
  main.dart

App Flow

  1. Scan Barcode: Camera scans → Show result below scanner
  2. Tap Result: Navigate to detail screen
  3. Fill Form: Enter data in 4 text fields
  4. Save: Call API to save data + store locally
  5. Print: Print form data

Data Models

class ScanItem {
  final String barcode;
  final DateTime timestamp;
  final String field1;
  final String field2;
  final String field3;
  final String field4;
  
  ScanItem({
    required this.barcode,
    required this.timestamp,
    this.field1 = '',
    this.field2 = '',
    this.field3 = '',
    this.field4 = '',
  });
}

class SaveRequest {
  final String barcode;
  final String field1;
  final String field2;
  final String field3;
  final String field4;
  
  SaveRequest({
    required this.barcode,
    required this.field1,
    required this.field2,
    required this.field3,
    required this.field4,
  });
  
  Map<String, dynamic> toJson() => {
    'barcode': barcode,
    'field1': field1,
    'field2': field2,
    'field3': field3,
    'field4': field4,
  };
}

Home Screen Layout

┌─────────────────────────┐
│                         │
│    Barcode Scanner      │
│     (Camera View)       │
│                         │
├─────────────────────────┤
│  Last Scanned: 123456   │
│    [Tap to edit]        │
├─────────────────────────┤
│    Scan History         │
│  • 123456 - 10:30 AM    │
│  • 789012 - 10:25 AM    │
│  • 345678 - 10:20 AM    │
│                         │
└─────────────────────────┘

Detail Screen Layout

┌─────────────────────────┐
│   Barcode: 123456789    │
├─────────────────────────┤
│                         │
│ Field 1: [____________] │
│                         │
│ Field 2: [____________] │
│                         │
│ Field 3: [____________] │
│                         │
│ Field 4: [____________] │
│                         │
├─────────────────────────┤
│   [Save]     [Print]    │
└─────────────────────────┘

Key Features

Barcode Scanner

  • Use mobile_scanner package
  • Support Code128 and common formats
  • Show scanned value immediately below scanner
  • Add to history list automatically

API Integration

  • Call API when Save button is pressed
  • Send form data to server
  • Handle success/error responses
  • Show loading state during API call

Local Storage

  • Use Hive for simple local storage
  • Store scan history with timestamps
  • Save form data after successful API call

Navigation

  • Tap on scan result → Navigate to detail screen
  • Pass barcode value to detail screen
  • Simple back navigation

State Management (Riverpod)

Scanner State

class ScannerState {
  final String? currentBarcode;
  final List<ScanItem> history;
  
  ScannerState({
    this.currentBarcode,
    this.history = const [],
  });
}

Form State

class FormState {
  final String barcode;
  final String field1;
  final String field2; 
  final String field3;
  final String field4;
  final bool isLoading;
  final String? error;
  
  FormState({
    required this.barcode,
    this.field1 = '',
    this.field2 = '',
    this.field3 = '',
    this.field4 = '',
    this.isLoading = false,
    this.error,
  });
}

Use Cases

Save Scan Use Case

class SaveScanUseCase {
  final ScannerRepository repository;
  
  SaveScanUseCase(this.repository);
  
  Future<Either<Failure, void>> call(SaveRequest request) async {
    return await repository.saveScan(request);
  }
}

Repository Pattern

abstract class ScannerRepository {
  Future<Either<Failure, void>> saveScan(SaveRequest request);
}

class ScannerRepositoryImpl implements ScannerRepository {
  final ScannerRemoteDataSource remoteDataSource;
  
  ScannerRepositoryImpl(this.remoteDataSource);
  
  @override
  Future<Either<Failure, void>> saveScan(SaveRequest request) async {
    try {
      await remoteDataSource.saveScan(request);
      return const Right(null);
    } catch (e) {
      return Left(ServerFailure(e.toString()));
    }
  }
}

Data Source

abstract class ScannerRemoteDataSource {
  Future<void> saveScan(SaveRequest request);
}

class ScannerRemoteDataSourceImpl implements ScannerRemoteDataSource {
  final ApiClient apiClient;
  
  ScannerRemoteDataSourceImpl(this.apiClient);
  
  @override
  Future<void> saveScan(SaveRequest request) async {
    final response = await apiClient.post(
      '/api/scans',
      data: request.toJson(),
    );
    
    if (response.statusCode != 200) {
      throw ServerException('Failed to save scan');
    }
  }
}

Widget Structure

Home Page

class HomePage extends ConsumerWidget {
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: Column(
        children: [
          // Barcode Scanner (top half)
          Expanded(
            flex: 1,
            child: BarcodeScannerWidget(),
          ),
          
          // Scan Result Display
          ScanResultDisplay(),
          
          // History List (bottom half)  
          Expanded(
            flex: 1,
            child: ScanHistoryList(),
          ),
        ],
      ),
    );
  }
}

Detail Page with Save API Call

class DetailPage extends ConsumerWidget {
  final String barcode;
  
  Widget build(BuildContext context, WidgetRef ref) {
    final formState = ref.watch(formProvider);
    
    return Scaffold(
      appBar: AppBar(title: Text('Edit Details')),
      body: Column(
        children: [
          // Barcode Header
          Container(
            padding: EdgeInsets.all(16),
            child: Text('Barcode: $barcode'),
          ),
          
          // 4 Text Fields
          Expanded(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                children: [
                  TextField(
                    decoration: InputDecoration(labelText: 'Field 1'),
                    onChanged: (value) => ref.read(formProvider.notifier).updateField1(value),
                  ),
                  TextField(
                    decoration: InputDecoration(labelText: 'Field 2'),
                    onChanged: (value) => ref.read(formProvider.notifier).updateField2(value),
                  ),
                  TextField(
                    decoration: InputDecoration(labelText: 'Field 3'),
                    onChanged: (value) => ref.read(formProvider.notifier).updateField3(value),
                  ),
                  TextField(
                    decoration: InputDecoration(labelText: 'Field 4'),
                    onChanged: (value) => ref.read(formProvider.notifier).updateField4(value),
                  ),
                ],
              ),
            ),
          ),
          
          // Error Message
          if (formState.error != null)
            Container(
              padding: EdgeInsets.all(16),
              child: Text(
                formState.error!,
                style: TextStyle(color: Colors.red),
              ),
            ),
          
          // Save & Print Buttons
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: formState.isLoading ? null : () => _saveData(ref),
                    child: formState.isLoading 
                      ? CircularProgressIndicator()
                      : Text('Save'),
                  ),
                ),
                SizedBox(width: 16),
                Expanded(
                  child: ElevatedButton(
                    onPressed: _printData,  
                    child: Text('Print'),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
  
  void _saveData(WidgetRef ref) async {
    final formState = ref.read(formProvider);
    final saveRequest = SaveRequest(
      barcode: barcode,
      field1: formState.field1,
      field2: formState.field2,
      field3: formState.field3,
      field4: formState.field4,
    );
    
    await ref.read(formProvider.notifier).saveData(saveRequest);
  }
}

Core Functions

Save Data with API Call

class FormNotifier extends StateNotifier<FormState> {
  final SaveScanUseCase saveScanUseCase;
  
  FormNotifier(this.saveScanUseCase, String barcode) 
    : super(FormState(barcode: barcode));
  
  Future<void> saveData(SaveRequest request) async {
    state = state.copyWith(isLoading: true, error: null);
    
    final result = await saveScanUseCase(request);
    
    result.fold(
      (failure) => state = state.copyWith(
        isLoading: false, 
        error: failure.message,
      ),
      (_) {
        state = state.copyWith(isLoading: false);
        // Save to local storage after successful API call
        _saveToLocal(request);
        // Navigate back or show success message
      },
    );
  }
  
  void _saveToLocal(SaveRequest request) {
    // Save to Hive local storage
    final scanItem = ScanItem(
      barcode: request.barcode,
      timestamp: DateTime.now(),
      field1: request.field1,
      field2: request.field2,
      field3: request.field3,
      field4: request.field4,
    );
    // Add to Hive box
  }
}

Print Data

void printData(ScanItem item) {
  // Format data for printing
  // Use platform printing service
}

Dependencies

dependencies:
  flutter_riverpod: ^2.4.9
  mobile_scanner: ^4.0.1
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  go_router: ^12.1.3
  dio: ^5.3.2
  dartz: ^0.10.1
  get_it: ^7.6.4
  
dev_dependencies:
  hive_generator: ^2.0.1
  build_runner: ^2.4.7

Error Handling

abstract class Failure {
  final String message;
  const Failure(this.message);
}

class ServerFailure extends Failure {
  const ServerFailure(String message) : super(message);
}

class NetworkFailure extends Failure {
  const NetworkFailure(String message) : super(message);
}

Key Points

  • Save button calls API to save form data
  • Show loading state during API call
  • Handle API errors with user-friendly messages
  • Save to local storage only after successful API call
  • Use clean architecture with use cases and repository pattern
  • Keep UI simple with proper error handling