App 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
- Flutter Setup - Development environment
- GS1 Digital Link - Parser implementation details
- System Architecture - Backend integration