App Architecture

Last updated: December 16, 2025 Clean Architecture

The QR Igniter Flutter app follows a clean architecture pattern with clear separation of concerns between presentation, business logic, and data layers.

Project Structure

flutter_app/lib/
├── app/                        # App Configuration
│   ├── app.dart               # Main app widget (MaterialApp)
│   ├── routes.dart            # GoRouter navigation config
│   └── theme.dart             # Material 3 theming
│
├── core/                       # Core Utilities
│   └── constants/
│       ├── app_colors.dart    # Color palette (#2563EB)
│       └── app_constants.dart # API URLs, timeouts
│
├── data/                       # Data Layer
│   ├── api/
│   │   ├── api_client.dart    # Dio HTTP client
│   │   └── qr_code_api.dart   # QR code endpoints
│   └── models/
│       ├── scan_result.dart   # Scan response model
│       └── competition_entry.dart
│
├── presentation/               # UI Layer
│   ├── screens/
│   │   ├── splash/            # Splash screen
│   │   ├── home/              # Main menu
│   │   ├── scanner/           # QR scanner
│   │   ├── product_info/      # Product display
│   │   └── competition/       # Entry form
│   └── widgets/
│       ├── menu_item_card.dart
│       └── scanner_overlay.dart
│
├── providers/                  # State Management
│   └── api_providers.dart     # Riverpod providers
│
├── services/                   # Business Logic
│   └── gs1_parser_service.dart # GS1 Digital Link parser
│
└── main.dart                   # Entry point

Architecture Layers

Presentation Layer

Handles UI rendering and user interaction:

  • Screens: Full-page widgets (SplashScreen, HomeScreen, etc.)
  • Widgets: Reusable UI components
  • Theme: Material 3 styling and colors

Business Logic Layer

Contains application logic and state:

  • Services: GS1 parser, validation logic
  • Providers: Riverpod state management

Data Layer

Manages data access and persistence:

  • API Client: HTTP requests via Dio
  • Models: Data transfer objects
  • Storage: Secure token storage

Navigation (GoRouter)

// lib/app/routes.dart
class AppRoutes {
  static const String splash = '/';
  static const String home = '/home';
  static const String scanner = '/scanner';
  static const String productInfo = '/product-info';
  static const String competition = '/competition';

  static final router = GoRouter(
    initialLocation: splash,
    routes: [
      GoRoute(path: splash, builder: (_, __) => const SplashScreen()),
      GoRoute(path: home, builder: (_, __) => const HomeScreen()),
      GoRoute(path: scanner, builder: (_, __) => const ScannerScreen()),
      GoRoute(
        path: productInfo,
        builder: (_, state) => ProductInfoScreen(
          parsedData: state.extra as Gs1ParsedData,
        ),
      ),
      GoRoute(
        path: competition,
        builder: (_, state) => CompetitionScreen(
          parsedData: state.extra as Gs1ParsedData,
        ),
      ),
    ],
  );
}

State Management (Riverpod)

// lib/providers/api_providers.dart
final apiClientProvider = Provider((ref) {
  return ApiClient();
});

final qrCodeApiProvider = Provider((ref) {
  final client = ref.watch(apiClientProvider);
  return QrCodeApi(client);
});

// Usage in widgets
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final api = ref.watch(qrCodeApiProvider);
    // ...
  }
}

API Client

// lib/data/api/api_client.dart
class ApiClient {
  late final Dio _dio;
  final FlutterSecureStorage _storage;

  ApiClient() : _storage = const FlutterSecureStorage() {
    _dio = Dio(BaseOptions(
      baseUrl: AppConstants.baseUrl,
      connectTimeout: AppConstants.connectTimeout,
      receiveTimeout: AppConstants.receiveTimeout,
    ));

    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await _storage.read(key: 'auth_token');
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        return handler.next(options);
      },
    ));
  }
}

GS1 Parser Service

// lib/services/gs1_parser_service.dart
class Gs1ParserService {
  Gs1ParsedData parse(String input) {
    // Parse GS1 Digital Link URIs
    // Parse raw barcode data with GS separators
    // Parse plain GTINs
    // Extract AI values (01, 10, 21, 17, 22)
  }

  String buildResolutionUrl(Gs1ParsedData data, String baseUrl) {
    // Build compliant GS1 Digital Link URI
  }
}

class Gs1ParsedData {
  final String? gtin;
  final String? batchLot;
  final String? serialNumber;
  final String? expiryDate;
  final String? cpv;
  final bool isValid;

  String? get gtin14 => gtin?.padLeft(14, '0');
  bool get isProductLevel => gtin != null && batchLot == null;
  bool get isBatchLevel => batchLot != null && serialNumber == null;
  bool get isSerializedLevel => serialNumber != null;
}

Screen Flow

SplashScreen (2s delay)
    │
    ▼
HomeScreen
    │
    ├──▶ ScannerScreen
    │        │
    │        ▼ (on scan)
    │    ProductInfoScreen
    │        │
    │        ▼ (enter competition)
    │    CompetitionScreen
    │        │
    │        ▼ (success)
    │    [Return to Home]
    │
    ├──▶ My Scans (future)
    ├──▶ Competitions (future)
    └──▶ Settings (future)

Theme Configuration

// lib/app/theme.dart
class AppTheme {
  static ThemeData get lightTheme {
    return ThemeData(
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(
        seedColor: AppColors.primary,  // #2563EB
        brightness: Brightness.light,
      ),
      appBarTheme: const AppBarTheme(
        centerTitle: true,
        elevation: 0,
      ),
      elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.symmetric(
            horizontal: 32,
            vertical: 16,
          ),
        ),
      ),
    );
  }
}

Testing Structure

test/
├── services/
│   └── gs1_parser_service_test.dart  # 31 tests
└── widget_test.dart                   # 2 tests

Total: 33 tests

Key Design Decisions

Decision Choice Rationale
State Management Riverpod Type-safe, testable, compile-time safety
Navigation GoRouter Declarative routing, deep linking support
HTTP Client Dio Interceptors, request/response transformation
QR Scanning mobile_scanner MLKit on Android, AVFoundation on iOS
Token Storage flutter_secure_storage Keychain (iOS), EncryptedSharedPreferences (Android)

Next Steps