@jeffrey2423/coding-standards 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1292 @@
1
+ # Mobile Flutter Development Standards
2
+
3
+ > **Note**: For shared architectural principles (Clean Architecture, DDD, design philosophy), refer to [architecture-patterns.md](./architecture-patterns.md). For design system (colors, typography, UX), refer to [technical-preferences-ux.md](./technical-preferences-ux.md).
4
+
5
+ ---
6
+
7
+ ## Technology Stack
8
+
9
+ ### Core Technologies
10
+
11
+ | Category | Technology | Version | Notes |
12
+ |----------|------------|---------|-------|
13
+ | **SDK** | Flutter | 3.27+ | Stable channel only |
14
+ | **Language** | Dart | 3.6+ | Sound null safety, records, patterns |
15
+ | **State** | Riverpod | 3+ | Code-generated, annotation-based |
16
+ | **Navigation** | GoRouter | 14+ | Declarative, deep-link ready |
17
+ | **DI** | get_it + injectable | Latest | Service locator with code generation |
18
+ | **HTTP** | Dio | 5+ | With interceptors |
19
+ | **Local DB** | Drift | 2+ | Type-safe SQLite, reactive streams |
20
+ | **Secure Storage** | flutter_secure_storage | 9+ | Keychain / Keystore backed |
21
+ | **Models** | Freezed + json_serializable | Latest | Immutable models, sealed classes |
22
+ | **Validation** | formz | Latest | Type-safe form field validation |
23
+ | **Testing** | flutter_test + mocktail | Latest | Unit, widget, integration |
24
+
25
+ ### Development Tools
26
+
27
+ | Tool | Purpose |
28
+ |------|---------|
29
+ | `flutter analyze` | Static analysis (strict) |
30
+ | `dart format` | Code formatting |
31
+ | `build_runner` | Code generation (Freezed, Riverpod, Drift) |
32
+ | `flutter_gen` | Type-safe asset generation |
33
+ | `very_good_cli` | Project scaffolding and CI |
34
+ | `patrol` | Integration and end-to-end testing |
35
+
36
+ ---
37
+
38
+ ## Architecture
39
+
40
+ ### Style: Clean Architecture + DDD + Feature-First
41
+
42
+ Same layered philosophy as the backend and frontend standards. Business logic drives every decision. Dependencies always point inward.
43
+
44
+ ```
45
+ ┌──────────────────────────────────────────────────────┐
46
+ │ PRESENTATION LAYER │
47
+ │ Widgets, Pages, Riverpod UI providers │
48
+ └──────────────────────────────────────────────────────┘
49
+
50
+
51
+ ┌──────────────────────────────────────────────────────┐
52
+ │ APPLICATION LAYER │
53
+ │ Use cases, state notifiers, mappers │
54
+ └──────────────────────────────────────────────────────┘
55
+
56
+
57
+ ┌──────────────────────────────────────────────────────┐
58
+ │ INFRASTRUCTURE LAYER │
59
+ │ Repository implementations, APIs, local DB │
60
+ └──────────────────────────────────────────────────────┘
61
+
62
+
63
+ ┌──────────────────────────────────────────────────────┐
64
+ │ DOMAIN LAYER │
65
+ │ Entities, value objects, repository interfaces │
66
+ └──────────────────────────────────────────────────────┘
67
+ ```
68
+
69
+ ### Folder Structure
70
+
71
+ ```
72
+ lib/
73
+ ├── main.dart # App entry point
74
+ ├── app/
75
+ │ ├── app.dart # MaterialApp / CupertinoApp
76
+ │ ├── router/
77
+ │ │ └── app_router.dart # GoRouter configuration
78
+ │ ├── providers/
79
+ │ │ └── app_providers.dart # Root Riverpod overrides
80
+ │ └── di/
81
+ │ ├── injection.dart # get_it setup
82
+ │ └── injection.config.dart # Generated by injectable
83
+
84
+ ├── modules/ # Business modules (DDD)
85
+ │ ├── sales/
86
+ │ │ ├── quotes/
87
+ │ │ │ ├── cart/
88
+ │ │ │ │ ├── domain/
89
+ │ │ │ │ │ ├── entities/
90
+ │ │ │ │ │ │ └── cart_item.dart
91
+ │ │ │ │ │ ├── repositories/
92
+ │ │ │ │ │ │ └── i_cart_repository.dart
93
+ │ │ │ │ │ ├── value_objects/
94
+ │ │ │ │ │ │ └── quantity.dart
95
+ │ │ │ │ │ └── failures/
96
+ │ │ │ │ │ └── cart_failure.dart
97
+ │ │ │ │ ├── application/
98
+ │ │ │ │ │ ├── use_cases/
99
+ │ │ │ │ │ │ ├── add_to_cart.dart
100
+ │ │ │ │ │ │ └── remove_from_cart.dart
101
+ │ │ │ │ │ └── providers/
102
+ │ │ │ │ │ └── cart_provider.dart
103
+ │ │ │ │ ├── infrastructure/
104
+ │ │ │ │ │ ├── repositories/
105
+ │ │ │ │ │ │ └── cart_repository.dart
106
+ │ │ │ │ │ ├── datasources/
107
+ │ │ │ │ │ │ ├── cart_remote_datasource.dart
108
+ │ │ │ │ │ │ └── cart_local_datasource.dart
109
+ │ │ │ │ │ └── models/
110
+ │ │ │ │ │ └── cart_item_model.dart
111
+ │ │ │ │ └── presentation/
112
+ │ │ │ │ ├── pages/
113
+ │ │ │ │ │ └── cart_page.dart
114
+ │ │ │ │ └── widgets/
115
+ │ │ │ │ └── cart_item_widget.dart
116
+ │ │ │ └── products/
117
+ │ │ └── billing/
118
+ │ ├── inventory/
119
+ │ └── users/
120
+
121
+ ├── shared/
122
+ │ ├── widgets/ # Reusable UI widgets
123
+ │ │ ├── app_button.dart
124
+ │ │ ├── app_text_field.dart
125
+ │ │ └── loading_skeleton.dart
126
+ │ ├── extensions/ # Dart extensions
127
+ │ │ ├── context_extensions.dart
128
+ │ │ └── string_extensions.dart
129
+ │ ├── utils/
130
+ │ │ ├── validators.dart
131
+ │ │ └── formatters.dart
132
+ │ ├── theme/
133
+ │ │ ├── app_theme.dart
134
+ │ │ ├── app_colors.dart
135
+ │ │ ├── app_typography.dart
136
+ │ │ └── app_spacing.dart
137
+ │ ├── constants/
138
+ │ │ └── app_constants.dart
139
+ │ └── failures/
140
+ │ └── failure.dart # Base failure sealed class
141
+
142
+ ├── core/
143
+ │ ├── network/
144
+ │ │ ├── dio_client.dart
145
+ │ │ └── interceptors/
146
+ │ │ ├── auth_interceptor.dart
147
+ │ │ └── logging_interceptor.dart
148
+ │ ├── storage/
149
+ │ │ ├── secure_storage.dart
150
+ │ │ └── app_database.dart # Drift database
151
+ │ └── services/
152
+ │ └── analytics_service.dart
153
+
154
+ └── l10n/ # Localization
155
+ ├── app_en.arb
156
+ └── app_es.arb
157
+
158
+ test/
159
+ ├── modules/
160
+ │ └── sales/
161
+ │ └── quotes/
162
+ │ └── cart/
163
+ │ ├── domain/
164
+ │ ├── application/
165
+ │ └── infrastructure/
166
+ ├── shared/
167
+ └── helpers/
168
+ └── test_helpers.dart
169
+
170
+ integration_test/
171
+ └── features/
172
+ └── cart_flow_test.dart
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Domain Layer
178
+
179
+ ### Entities
180
+
181
+ Entities are immutable. Use `Freezed` for boilerplate elimination and value equality.
182
+
183
+ ```dart
184
+ // modules/sales/quotes/cart/domain/entities/cart_item.dart
185
+ import 'package:freezed_annotation/freezed_annotation.dart';
186
+
187
+ part 'cart_item.freezed.dart';
188
+
189
+ @freezed
190
+ class CartItem with _$CartItem {
191
+ const CartItem._(); // Custom methods require private constructor
192
+
193
+ const factory CartItem({
194
+ required String id,
195
+ required String productId,
196
+ required String productName,
197
+ required int quantity,
198
+ required double unitPrice,
199
+ }) = _CartItem;
200
+
201
+ double get totalPrice => unitPrice * quantity;
202
+ bool get isValid => quantity > 0 && unitPrice > 0;
203
+ }
204
+ ```
205
+
206
+ ### Value Objects
207
+
208
+ Value objects enforce business invariants at construction time and return failures, never throw.
209
+
210
+ ```dart
211
+ // modules/sales/quotes/cart/domain/value_objects/quantity.dart
212
+ import 'package:dartz/dartz.dart';
213
+ import '../failures/cart_failure.dart';
214
+
215
+ class Quantity {
216
+ final Either<CartFailure, int> value;
217
+
218
+ factory Quantity(int input) {
219
+ return Quantity._(
220
+ input >= 1
221
+ ? right(input)
222
+ : left(const CartFailure.invalidQuantity()),
223
+ );
224
+ }
225
+
226
+ const Quantity._(this.value);
227
+
228
+ int getOrCrash() => value.fold(
229
+ (f) => throw Exception('Unvalidated quantity: $f'),
230
+ (v) => v,
231
+ );
232
+
233
+ bool isValid() => value.isRight();
234
+ }
235
+ ```
236
+
237
+ ### Failures (Sealed Classes)
238
+
239
+ Use Dart 3 sealed classes for exhaustive failure modeling. Never throw domain exceptions.
240
+
241
+ ```dart
242
+ // modules/sales/quotes/cart/domain/failures/cart_failure.dart
243
+ sealed class CartFailure {
244
+ const CartFailure();
245
+ }
246
+
247
+ class InvalidQuantity extends CartFailure {
248
+ const InvalidQuantity();
249
+ }
250
+
251
+ class ProductNotFound extends CartFailure {
252
+ final String productId;
253
+ const ProductNotFound(this.productId);
254
+ }
255
+
256
+ class CartLimitExceeded extends CartFailure {
257
+ final int maxItems;
258
+ const CartLimitExceeded(this.maxItems);
259
+ }
260
+
261
+ class ServerFailure extends CartFailure {
262
+ final String message;
263
+ const ServerFailure(this.message);
264
+ }
265
+ ```
266
+
267
+ ### Repository Interfaces
268
+
269
+ ```dart
270
+ // modules/sales/quotes/cart/domain/repositories/i_cart_repository.dart
271
+ import 'package:dartz/dartz.dart';
272
+ import '../entities/cart_item.dart';
273
+ import '../failures/cart_failure.dart';
274
+
275
+ abstract interface class ICartRepository {
276
+ Future<Either<CartFailure, List<CartItem>>> getCartItems();
277
+ Future<Either<CartFailure, CartItem>> addItem({
278
+ required String productId,
279
+ required int quantity,
280
+ });
281
+ Future<Either<CartFailure, Unit>> removeItem(String itemId);
282
+ Future<Either<CartFailure, Unit>> clearCart();
283
+ Stream<List<CartItem>> watchCartItems();
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Application Layer
290
+
291
+ ### Use Cases
292
+
293
+ Each use case is a single callable class. One use case per file.
294
+
295
+ ```dart
296
+ // modules/sales/quotes/cart/application/use_cases/add_to_cart.dart
297
+ import 'package:dartz/dartz.dart';
298
+ import 'package:injectable/injectable.dart';
299
+ import '../../domain/entities/cart_item.dart';
300
+ import '../../domain/failures/cart_failure.dart';
301
+ import '../../domain/repositories/i_cart_repository.dart';
302
+ import '../../domain/value_objects/quantity.dart';
303
+
304
+ @injectable
305
+ class AddToCart {
306
+ final ICartRepository _repository;
307
+
308
+ const AddToCart(this._repository);
309
+
310
+ Future<Either<CartFailure, CartItem>> call({
311
+ required String productId,
312
+ required int quantity,
313
+ }) async {
314
+ final quantityVO = Quantity(quantity);
315
+
316
+ if (!quantityVO.isValid()) {
317
+ return left(const InvalidQuantity());
318
+ }
319
+
320
+ return _repository.addItem(
321
+ productId: productId,
322
+ quantity: quantityVO.getOrCrash(),
323
+ );
324
+ }
325
+ }
326
+ ```
327
+
328
+ ### Riverpod Providers (State Notifiers)
329
+
330
+ Use `@riverpod` annotation with code generation. `AsyncNotifier` for async state, `Notifier` for sync.
331
+
332
+ ```dart
333
+ // modules/sales/quotes/cart/application/providers/cart_provider.dart
334
+ import 'package:riverpod_annotation/riverpod_annotation.dart';
335
+ import '../../domain/entities/cart_item.dart';
336
+ import '../../domain/failures/cart_failure.dart';
337
+ import '../use_cases/add_to_cart.dart';
338
+ import '../use_cases/remove_from_cart.dart';
339
+
340
+ part 'cart_provider.g.dart';
341
+
342
+ @Riverpod(keepAlive: true)
343
+ class CartNotifier extends _$CartNotifier {
344
+ late final AddToCart _addToCart;
345
+ late final RemoveFromCart _removeFromCart;
346
+
347
+ @override
348
+ Future<List<CartItem>> build() async {
349
+ _addToCart = ref.read(addToCartProvider);
350
+ _removeFromCart = ref.read(removeFromCartProvider);
351
+
352
+ // Watch reactive stream from local DB
353
+ final repository = ref.read(cartRepositoryProvider);
354
+ ref.listen(
355
+ Stream.fromFuture(repository.getCartItems().then(
356
+ (e) => e.fold((_) => <CartItem>[], (v) => v),
357
+ )).asStream().asBroadcastStream(),
358
+ (_, __) {},
359
+ );
360
+
361
+ final result = await repository.getCartItems();
362
+ return result.fold(
363
+ (failure) => throw _mapFailure(failure),
364
+ (items) => items,
365
+ );
366
+ }
367
+
368
+ Future<void> addItem({
369
+ required String productId,
370
+ required int quantity,
371
+ }) async {
372
+ state = const AsyncLoading();
373
+ final result = await _addToCart(
374
+ productId: productId,
375
+ quantity: quantity,
376
+ );
377
+ state = result.fold(
378
+ (failure) => AsyncError(_mapFailure(failure), StackTrace.current),
379
+ (_) => AsyncData([...state.valueOrNull ?? [], _]),
380
+ );
381
+ }
382
+
383
+ Exception _mapFailure(CartFailure failure) => switch (failure) {
384
+ InvalidQuantity() => Exception('La cantidad debe ser mayor a cero'),
385
+ ProductNotFound(:final productId) =>
386
+ Exception('Producto $productId no encontrado'),
387
+ CartLimitExceeded(:final maxItems) =>
388
+ Exception('El carrito no puede tener más de $maxItems artículos'),
389
+ ServerFailure(:final message) => Exception(message),
390
+ };
391
+ }
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Infrastructure Layer
397
+
398
+ ### Models (DTO + JSON)
399
+
400
+ ```dart
401
+ // modules/sales/quotes/cart/infrastructure/models/cart_item_model.dart
402
+ import 'package:freezed_annotation/freezed_annotation.dart';
403
+ import '../../domain/entities/cart_item.dart';
404
+
405
+ part 'cart_item_model.freezed.dart';
406
+ part 'cart_item_model.g.dart';
407
+
408
+ @freezed
409
+ class CartItemModel with _$CartItemModel {
410
+ const CartItemModel._();
411
+
412
+ const factory CartItemModel({
413
+ required String id,
414
+ @JsonKey(name: 'product_id') required String productId,
415
+ @JsonKey(name: 'product_name') required String productName,
416
+ required int quantity,
417
+ @JsonKey(name: 'unit_price') required double unitPrice,
418
+ }) = _CartItemModel;
419
+
420
+ factory CartItemModel.fromJson(Map<String, dynamic> json) =>
421
+ _$CartItemModelFromJson(json);
422
+
423
+ factory CartItemModel.fromDomain(CartItem entity) => CartItemModel(
424
+ id: entity.id,
425
+ productId: entity.productId,
426
+ productName: entity.productName,
427
+ quantity: entity.quantity,
428
+ unitPrice: entity.unitPrice,
429
+ );
430
+
431
+ CartItem toDomain() => CartItem(
432
+ id: id,
433
+ productId: productId,
434
+ productName: productName,
435
+ quantity: quantity,
436
+ unitPrice: unitPrice,
437
+ );
438
+ }
439
+ ```
440
+
441
+ ### Repository Implementation
442
+
443
+ ```dart
444
+ // modules/sales/quotes/cart/infrastructure/repositories/cart_repository.dart
445
+ import 'package:dartz/dartz.dart';
446
+ import 'package:injectable/injectable.dart';
447
+ import '../../domain/entities/cart_item.dart';
448
+ import '../../domain/failures/cart_failure.dart';
449
+ import '../../domain/repositories/i_cart_repository.dart';
450
+ import '../datasources/cart_remote_datasource.dart';
451
+ import '../datasources/cart_local_datasource.dart';
452
+
453
+ @LazySingleton(as: ICartRepository)
454
+ class CartRepository implements ICartRepository {
455
+ final CartRemoteDatasource _remote;
456
+ final CartLocalDatasource _local;
457
+
458
+ const CartRepository(this._remote, this._local);
459
+
460
+ @override
461
+ Future<Either<CartFailure, List<CartItem>>> getCartItems() async {
462
+ try {
463
+ final local = await _local.getCartItems();
464
+ if (local.isNotEmpty) return right(local);
465
+
466
+ final remote = await _remote.getCartItems();
467
+ await _local.cacheCartItems(remote);
468
+ return right(remote.map((m) => m.toDomain()).toList());
469
+ } on NetworkException catch (e) {
470
+ return left(ServerFailure(e.message));
471
+ } catch (_) {
472
+ return left(const ServerFailure('Error inesperado'));
473
+ }
474
+ }
475
+
476
+ @override
477
+ Future<Either<CartFailure, CartItem>> addItem({
478
+ required String productId,
479
+ required int quantity,
480
+ }) async {
481
+ try {
482
+ final model = await _remote.addItem(
483
+ productId: productId,
484
+ quantity: quantity,
485
+ );
486
+ await _local.saveItem(model);
487
+ return right(model.toDomain());
488
+ } on NetworkException catch (e) {
489
+ return left(ServerFailure(e.message));
490
+ }
491
+ }
492
+
493
+ @override
494
+ Future<Either<CartFailure, Unit>> removeItem(String itemId) async {
495
+ try {
496
+ await _remote.removeItem(itemId);
497
+ await _local.deleteItem(itemId);
498
+ return right(unit);
499
+ } on NetworkException catch (e) {
500
+ return left(ServerFailure(e.message));
501
+ }
502
+ }
503
+
504
+ @override
505
+ Future<Either<CartFailure, Unit>> clearCart() async {
506
+ try {
507
+ await _remote.clearCart();
508
+ await _local.clearAll();
509
+ return right(unit);
510
+ } on NetworkException catch (e) {
511
+ return left(ServerFailure(e.message));
512
+ }
513
+ }
514
+
515
+ @override
516
+ Stream<List<CartItem>> watchCartItems() =>
517
+ _local.watchCartItems().map((items) => items.map((m) => m.toDomain()).toList());
518
+ }
519
+ ```
520
+
521
+ ### Dio HTTP Client
522
+
523
+ ```dart
524
+ // core/network/dio_client.dart
525
+ import 'package:dio/dio.dart';
526
+ import 'package:injectable/injectable.dart';
527
+ import 'interceptors/auth_interceptor.dart';
528
+ import 'interceptors/logging_interceptor.dart';
529
+
530
+ @singleton
531
+ class DioClient {
532
+ late final Dio dio;
533
+
534
+ DioClient(AuthInterceptor authInterceptor) {
535
+ dio = Dio(
536
+ BaseOptions(
537
+ baseUrl: const String.fromEnvironment('API_BASE_URL'),
538
+ connectTimeout: const Duration(seconds: 10),
539
+ receiveTimeout: const Duration(seconds: 30),
540
+ contentType: 'application/json',
541
+ ),
542
+ );
543
+
544
+ dio.interceptors.addAll([
545
+ authInterceptor,
546
+ LoggingInterceptor(),
547
+ RetryInterceptor(dio: dio, retries: 3),
548
+ ]);
549
+ }
550
+ }
551
+ ```
552
+
553
+ ---
554
+
555
+ ## Presentation Layer
556
+
557
+ ### Pages
558
+
559
+ Pages are thin orchestrators. They connect Riverpod state to the widget tree and handle routing.
560
+
561
+ ```dart
562
+ // modules/sales/quotes/cart/presentation/pages/cart_page.dart
563
+ import 'package:flutter/material.dart';
564
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
565
+ import 'package:go_router/go_router.dart';
566
+ import '../../application/providers/cart_provider.dart';
567
+ import '../widgets/cart_item_widget.dart';
568
+ import '../../../../shared/widgets/loading_skeleton.dart';
569
+ import '../../../../shared/widgets/error_view.dart';
570
+
571
+ class CartPage extends ConsumerWidget {
572
+ const CartPage({super.key});
573
+
574
+ @override
575
+ Widget build(BuildContext context, WidgetRef ref) {
576
+ final cartState = ref.watch(cartNotifierProvider);
577
+
578
+ return Scaffold(
579
+ appBar: AppBar(title: const Text('Carrito')),
580
+ body: switch (cartState) {
581
+ AsyncLoading() => const LoadingSkeleton(),
582
+ AsyncError(:final error) => ErrorView(
583
+ message: error.toString(),
584
+ onRetry: () => ref.invalidate(cartNotifierProvider),
585
+ ),
586
+ AsyncData(:final value) when value.isEmpty => const _EmptyCart(),
587
+ AsyncData(:final value) => ListView.builder(
588
+ itemCount: value.length,
589
+ itemBuilder: (context, index) => CartItemWidget(
590
+ item: value[index],
591
+ onRemove: (id) => ref
592
+ .read(cartNotifierProvider.notifier)
593
+ .removeItem(id),
594
+ ),
595
+ ),
596
+ },
597
+ bottomNavigationBar: _CartSummaryBar(
598
+ onCheckout: () => context.push('/sales/checkout'),
599
+ ),
600
+ );
601
+ }
602
+ }
603
+ ```
604
+
605
+ ### Widgets
606
+
607
+ ```dart
608
+ // modules/sales/quotes/cart/presentation/widgets/cart_item_widget.dart
609
+ import 'package:flutter/material.dart';
610
+ import '../../domain/entities/cart_item.dart';
611
+
612
+ class CartItemWidget extends StatelessWidget {
613
+ final CartItem item;
614
+ final void Function(String id) onRemove;
615
+
616
+ const CartItemWidget({
617
+ super.key,
618
+ required this.item,
619
+ required this.onRemove,
620
+ });
621
+
622
+ @override
623
+ Widget build(BuildContext context) {
624
+ return ListTile(
625
+ key: ValueKey(item.id),
626
+ title: Text(item.productName),
627
+ subtitle: Text('Cantidad: ${item.quantity}'),
628
+ trailing: Row(
629
+ mainAxisSize: MainAxisSize.min,
630
+ children: [
631
+ Text('\$${item.totalPrice.toStringAsFixed(2)}'),
632
+ IconButton(
633
+ icon: const Icon(Icons.delete_outline),
634
+ tooltip: 'Eliminar del carrito',
635
+ onPressed: () => onRemove(item.id),
636
+ ),
637
+ ],
638
+ ),
639
+ );
640
+ }
641
+ }
642
+ ```
643
+
644
+ ---
645
+
646
+ ## Navigation (GoRouter)
647
+
648
+ ```dart
649
+ // app/router/app_router.dart
650
+ import 'package:flutter/material.dart';
651
+ import 'package:go_router/go_router.dart';
652
+ import 'package:riverpod_annotation/riverpod_annotation.dart';
653
+
654
+ part 'app_router.g.dart';
655
+
656
+ @riverpod
657
+ GoRouter appRouter(AppRouterRef ref) {
658
+ final isAuthenticated = ref.watch(authStateProvider);
659
+
660
+ return GoRouter(
661
+ initialLocation: '/dashboard',
662
+ redirect: (context, state) {
663
+ final loggedIn = isAuthenticated.valueOrNull ?? false;
664
+ final isAuthRoute = state.matchedLocation.startsWith('/auth');
665
+
666
+ if (!loggedIn && !isAuthRoute) return '/auth/login';
667
+ if (loggedIn && isAuthRoute) return '/dashboard';
668
+ return null;
669
+ },
670
+ routes: [
671
+ GoRoute(
672
+ path: '/auth/login',
673
+ builder: (context, state) => const LoginPage(),
674
+ ),
675
+ ShellRoute(
676
+ builder: (context, state, child) => AppShell(child: child),
677
+ routes: [
678
+ GoRoute(
679
+ path: '/dashboard',
680
+ builder: (context, state) => const DashboardPage(),
681
+ ),
682
+ GoRoute(
683
+ path: '/sales/quotes',
684
+ builder: (context, state) => const QuotesPage(),
685
+ routes: [
686
+ GoRoute(
687
+ path: 'cart',
688
+ builder: (context, state) => const CartPage(),
689
+ ),
690
+ ],
691
+ ),
692
+ GoRoute(
693
+ path: '/inventory/products',
694
+ builder: (context, state) => const ProductsPage(),
695
+ ),
696
+ ],
697
+ ),
698
+ ],
699
+ );
700
+ }
701
+ ```
702
+
703
+ ---
704
+
705
+ ## Design System
706
+
707
+ Mirrors the shared design tokens from `technical-preferences-ux.md`.
708
+
709
+ ### Colors
710
+
711
+ ```dart
712
+ // shared/theme/app_colors.dart
713
+ import 'package:flutter/material.dart';
714
+
715
+ abstract final class AppColors {
716
+ // Primary brand: #0E79FD
717
+ static const primary50 = Color(0xFFEFF8FF);
718
+ static const primary100 = Color(0xFFDBF0FF);
719
+ static const primary200 = Color(0xFFBFE3FF);
720
+ static const primary300 = Color(0xFF93D2FF);
721
+ static const primary400 = Color(0xFF60B6FF);
722
+ static const primary500 = Color(0xFF0E79FD); // Main
723
+ static const primary600 = Color(0xFF0B6AE6);
724
+ static const primary700 = Color(0xFF0959C2);
725
+ static const primary800 = Color(0xFF0E4A9E);
726
+ static const primary900 = Color(0xFF123F80);
727
+ static const primary950 = Color(0xFF11274D);
728
+
729
+ // Tertiary brand: #154ca9
730
+ static const tertiary700 = Color(0xFF154CA9); // Main
731
+
732
+ // Secondary brand: #000000
733
+ static const secondary950 = Color(0xFF000000); // Main
734
+
735
+ // Surfaces
736
+ static const backgroundLight = Color(0xFFFFFFFF);
737
+ static const surfaceLight = Color(0xFFF8FAFC); // slate-50
738
+ static const borderLight = Color(0xFFE2E8F0); // slate-200
739
+
740
+ static const backgroundDark = Color(0xFF020617); // slate-950
741
+ static const surfaceDark = Color(0xFF0F172A); // slate-900
742
+ static const borderDark = Color(0xFF334155); // slate-700
743
+
744
+ // Semantic
745
+ static const success = Color(0xFF22C55E); // green-500
746
+ static const warning = Color(0xFFF59E0B); // amber-500
747
+ static const error = Color(0xFFEF4444); // red-500
748
+ static const info = Color(0xFF06B6D4); // cyan-500
749
+ }
750
+ ```
751
+
752
+ ### Typography
753
+
754
+ Only 3 Inter weights are available per `technical-preferences-ux.md`.
755
+
756
+ ```dart
757
+ // shared/theme/app_typography.dart
758
+ import 'package:flutter/material.dart';
759
+
760
+ abstract final class AppTypography {
761
+ static const _fontFamily = 'Inter';
762
+
763
+ static const h1 = TextStyle(fontFamily: _fontFamily, fontSize: 36, fontWeight: FontWeight.w700, height: 1.2);
764
+ static const h2 = TextStyle(fontFamily: _fontFamily, fontSize: 30, fontWeight: FontWeight.w700, height: 1.2);
765
+ static const h3 = TextStyle(fontFamily: _fontFamily, fontSize: 24, fontWeight: FontWeight.w600, height: 1.3);
766
+ static const h4 = TextStyle(fontFamily: _fontFamily, fontSize: 20, fontWeight: FontWeight.w600, height: 1.3);
767
+ static const h5 = TextStyle(fontFamily: _fontFamily, fontSize: 18, fontWeight: FontWeight.w500, height: 1.4);
768
+ static const h6 = TextStyle(fontFamily: _fontFamily, fontSize: 16, fontWeight: FontWeight.w500, height: 1.4);
769
+
770
+ static const bodyLarge = TextStyle(fontFamily: _fontFamily, fontSize: 18, fontWeight: FontWeight.w400, height: 1.6);
771
+ static const body = TextStyle(fontFamily: _fontFamily, fontSize: 16, fontWeight: FontWeight.w400, height: 1.6);
772
+ static const bodySmall = TextStyle(fontFamily: _fontFamily, fontSize: 14, fontWeight: FontWeight.w400, height: 1.5);
773
+ static const caption = TextStyle(fontFamily: _fontFamily, fontSize: 12, fontWeight: FontWeight.w300, height: 1.4);
774
+
775
+ static const label = TextStyle(fontFamily: _fontFamily, fontSize: 14, fontWeight: FontWeight.w500, height: 1.4);
776
+ static const buttonLg = TextStyle(fontFamily: _fontFamily, fontSize: 16, fontWeight: FontWeight.w600, height: 1.0);
777
+ static const buttonSm = TextStyle(fontFamily: _fontFamily, fontSize: 14, fontWeight: FontWeight.w500, height: 1.0);
778
+ static const badge = TextStyle(fontFamily: _fontFamily, fontSize: 12, fontWeight: FontWeight.w700, height: 1.0);
779
+ }
780
+ ```
781
+
782
+ ### Theme
783
+
784
+ ```dart
785
+ // shared/theme/app_theme.dart
786
+ import 'package:flutter/material.dart';
787
+ import 'app_colors.dart';
788
+ import 'app_typography.dart';
789
+
790
+ abstract final class AppTheme {
791
+ static ThemeData get light => ThemeData(
792
+ useMaterial3: true,
793
+ fontFamily: 'Inter',
794
+ colorScheme: ColorScheme.fromSeed(
795
+ seedColor: AppColors.primary500,
796
+ brightness: Brightness.light,
797
+ primary: AppColors.primary500,
798
+ secondary: AppColors.tertiary700,
799
+ error: AppColors.error,
800
+ surface: AppColors.surfaceLight,
801
+ ),
802
+ scaffoldBackgroundColor: AppColors.backgroundLight,
803
+ appBarTheme: const AppBarTheme(
804
+ backgroundColor: AppColors.backgroundLight,
805
+ foregroundColor: Colors.black,
806
+ elevation: 0,
807
+ titleTextStyle: AppTypography.h5,
808
+ ),
809
+ elevatedButtonTheme: ElevatedButtonThemeData(
810
+ style: ElevatedButton.styleFrom(
811
+ backgroundColor: AppColors.primary500,
812
+ foregroundColor: Colors.white,
813
+ textStyle: AppTypography.buttonLg,
814
+ minimumSize: const Size(0, 48),
815
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
816
+ ),
817
+ ),
818
+ inputDecorationTheme: InputDecorationTheme(
819
+ border: OutlineInputBorder(
820
+ borderRadius: BorderRadius.circular(8),
821
+ borderSide: const BorderSide(color: AppColors.borderLight),
822
+ ),
823
+ labelStyle: AppTypography.label,
824
+ ),
825
+ textTheme: const TextTheme(
826
+ displayLarge: AppTypography.h1,
827
+ displayMedium: AppTypography.h2,
828
+ displaySmall: AppTypography.h3,
829
+ headlineMedium: AppTypography.h4,
830
+ headlineSmall: AppTypography.h5,
831
+ titleLarge: AppTypography.h6,
832
+ bodyLarge: AppTypography.bodyLarge,
833
+ bodyMedium: AppTypography.body,
834
+ bodySmall: AppTypography.bodySmall,
835
+ labelLarge: AppTypography.buttonLg,
836
+ labelSmall: AppTypography.badge,
837
+ ),
838
+ );
839
+
840
+ static ThemeData get dark => ThemeData(
841
+ useMaterial3: true,
842
+ fontFamily: 'Inter',
843
+ colorScheme: ColorScheme.fromSeed(
844
+ seedColor: AppColors.primary500,
845
+ brightness: Brightness.dark,
846
+ primary: AppColors.primary400,
847
+ secondary: AppColors.tertiary700,
848
+ error: AppColors.error,
849
+ surface: AppColors.surfaceDark,
850
+ ),
851
+ scaffoldBackgroundColor: AppColors.backgroundDark,
852
+ );
853
+ }
854
+ ```
855
+
856
+ ---
857
+
858
+ ## Local Database (Drift)
859
+
860
+ ```dart
861
+ // core/storage/app_database.dart
862
+ import 'package:drift/drift.dart';
863
+ import 'package:drift/native.dart';
864
+ import 'package:injectable/injectable.dart';
865
+ import 'package:path_provider/path_provider.dart';
866
+ import 'package:path/path.dart' as p;
867
+ import 'dart:io';
868
+
869
+ part 'app_database.g.dart';
870
+
871
+ // Table definitions follow snake_case automatically
872
+ class CartItems extends Table {
873
+ TextColumn get id => text()();
874
+ TextColumn get productId => text()();
875
+ TextColumn get productName => text()();
876
+ IntColumn get quantity => integer()();
877
+ RealColumn get unitPrice => real()();
878
+
879
+ @override
880
+ Set<Column> get primaryKey => {id};
881
+ }
882
+
883
+ @DriftDatabase(tables: [CartItems])
884
+ @singleton
885
+ class AppDatabase extends _$AppDatabase {
886
+ AppDatabase() : super(_openConnection());
887
+
888
+ @override
889
+ int get schemaVersion => 1;
890
+
891
+ Future<List<CartItemData>> getAllCartItems() =>
892
+ select(cartItems).get();
893
+
894
+ Stream<List<CartItemData>> watchAllCartItems() =>
895
+ select(cartItems).watch();
896
+
897
+ Future<int> upsertCartItem(CartItemsCompanion item) =>
898
+ into(cartItems).insertOnConflictUpdate(item);
899
+
900
+ Future<int> deleteCartItem(String id) =>
901
+ (delete(cartItems)..where((t) => t.id.equals(id))).go();
902
+
903
+ Future<int> clearAllCartItems() =>
904
+ delete(cartItems).go();
905
+
906
+ static QueryExecutor _openConnection() {
907
+ return LazyDatabase(() async {
908
+ final dir = await getApplicationDocumentsDirectory();
909
+ final file = File(p.join(dir.path, 'app.db'));
910
+ return NativeDatabase.createInBackground(file);
911
+ });
912
+ }
913
+ }
914
+ ```
915
+
916
+ ---
917
+
918
+ ## Dependency Injection
919
+
920
+ ```dart
921
+ // app/di/injection.dart
922
+ import 'package:get_it/get_it.dart';
923
+ import 'package:injectable/injectable.dart';
924
+ import 'injection.config.dart';
925
+
926
+ final getIt = GetIt.instance;
927
+
928
+ @InjectableInit(
929
+ initializerName: 'init',
930
+ preferRelativeImports: true,
931
+ asExtension: true,
932
+ )
933
+ Future<void> configureDependencies() async => getIt.init();
934
+ ```
935
+
936
+ Annotate classes with `@injectable`, `@singleton`, `@lazySingleton`, `@factoryMethod` as appropriate. Run `dart run build_runner build --delete-conflicting-outputs` to regenerate.
937
+
938
+ ---
939
+
940
+ ## Testing Standards
941
+
942
+ ### Strategy
943
+
944
+ | Type | Tool | Coverage Target | What to Test |
945
+ |------|------|-----------------|--------------|
946
+ | **Unit** | `flutter_test` + `mocktail` | > 80% | Entities, value objects, use cases |
947
+ | **Widget** | `flutter_test` | Medium | Key widgets and user interactions |
948
+ | **Integration** | `patrol` | Low | Critical user journeys end-to-end |
949
+
950
+ ### Unit Test Example
951
+
952
+ ```dart
953
+ // test/modules/sales/quotes/cart/application/use_cases/add_to_cart_test.dart
954
+ import 'package:dartz/dartz.dart';
955
+ import 'package:flutter_test/flutter_test.dart';
956
+ import 'package:mocktail/mocktail.dart';
957
+
958
+ class MockCartRepository extends Mock implements ICartRepository {}
959
+
960
+ void main() {
961
+ late MockCartRepository mockRepository;
962
+ late AddToCart useCase;
963
+
964
+ setUp(() {
965
+ mockRepository = MockCartRepository();
966
+ useCase = AddToCart(mockRepository);
967
+ });
968
+
969
+ group('AddToCart', () {
970
+ test('returns CartItem when quantity is valid', () async {
971
+ const productId = 'prod-1';
972
+ const quantity = 2;
973
+ final item = CartItem(
974
+ id: 'item-1',
975
+ productId: productId,
976
+ productName: 'Test Product',
977
+ quantity: quantity,
978
+ unitPrice: 10.0,
979
+ );
980
+
981
+ when(() => mockRepository.addItem(
982
+ productId: productId,
983
+ quantity: quantity,
984
+ )).thenAnswer((_) async => right(item));
985
+
986
+ final result = await useCase(
987
+ productId: productId,
988
+ quantity: quantity,
989
+ );
990
+
991
+ expect(result, right(item));
992
+ verify(() => mockRepository.addItem(
993
+ productId: productId,
994
+ quantity: quantity,
995
+ )).called(1);
996
+ });
997
+
998
+ test('returns InvalidQuantity failure when quantity is zero', () async {
999
+ final result = await useCase(productId: 'prod-1', quantity: 0);
1000
+
1001
+ expect(result, left(const InvalidQuantity()));
1002
+ verifyNever(() => mockRepository.addItem(
1003
+ productId: any(named: 'productId'),
1004
+ quantity: any(named: 'quantity'),
1005
+ ));
1006
+ });
1007
+ });
1008
+ }
1009
+ ```
1010
+
1011
+ ### Widget Test Example
1012
+
1013
+ ```dart
1014
+ // test/modules/sales/quotes/cart/presentation/widgets/cart_item_widget_test.dart
1015
+ import 'package:flutter/material.dart';
1016
+ import 'package:flutter_test/flutter_test.dart';
1017
+
1018
+ void main() {
1019
+ group('CartItemWidget', () {
1020
+ testWidgets('displays product name and total price', (tester) async {
1021
+ const item = CartItem(
1022
+ id: '1',
1023
+ productId: 'p1',
1024
+ productName: 'Producto Test',
1025
+ quantity: 2,
1026
+ unitPrice: 50.0,
1027
+ );
1028
+
1029
+ var removedId = '';
1030
+
1031
+ await tester.pumpWidget(
1032
+ MaterialApp(
1033
+ home: Scaffold(
1034
+ body: CartItemWidget(
1035
+ item: item,
1036
+ onRemove: (id) => removedId = id,
1037
+ ),
1038
+ ),
1039
+ ),
1040
+ );
1041
+
1042
+ expect(find.text('Producto Test'), findsOneWidget);
1043
+ expect(find.text('\$100.00'), findsOneWidget);
1044
+
1045
+ await tester.tap(find.byIcon(Icons.delete_outline));
1046
+ await tester.pump();
1047
+
1048
+ expect(removedId, '1');
1049
+ });
1050
+ });
1051
+ }
1052
+ ```
1053
+
1054
+ ---
1055
+
1056
+ ## Error Handling
1057
+
1058
+ ### Global Errors
1059
+
1060
+ Register a `FlutterError.onError` and `PlatformDispatcher.instance.onError` handler in `main.dart`:
1061
+
1062
+ ```dart
1063
+ // main.dart
1064
+ Future<void> main() async {
1065
+ WidgetsFlutterBinding.ensureInitialized();
1066
+
1067
+ FlutterError.onError = (details) {
1068
+ // Log to Crashlytics / Sentry
1069
+ FirebaseCrashlytics.instance.recordFlutterFatalError(details);
1070
+ };
1071
+
1072
+ PlatformDispatcher.instance.onError = (error, stack) {
1073
+ FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
1074
+ return true;
1075
+ };
1076
+
1077
+ await configureDependencies();
1078
+ runApp(
1079
+ ProviderScope(
1080
+ child: const App(),
1081
+ ),
1082
+ );
1083
+ }
1084
+ ```
1085
+
1086
+ ### UI-Level Error Handling
1087
+
1088
+ Use `AsyncValue`'s pattern matching (as shown in the page example above). Never expose raw exception messages to the UI. Map domain failures to user-friendly Spanish strings in the notifier.
1089
+
1090
+ ---
1091
+
1092
+ ## Security Standards
1093
+
1094
+ | Concern | Solution |
1095
+ |---------|---------|
1096
+ | Token storage | `flutter_secure_storage` (Keychain/Keystore) |
1097
+ | Certificate pinning | `dio_certificate_pinner` |
1098
+ | Screenshot prevention | `FlutterWindowManager.addFlags` (Android) |
1099
+ | Root/jailbreak detection | `flutter_jailbreak_detection` |
1100
+ | Sensitive data in logs | Redact with `LoggingInterceptor` |
1101
+ | Obfuscation | `flutter build --obfuscate --split-debug-info` |
1102
+
1103
+ ---
1104
+
1105
+ ## Performance Standards
1106
+
1107
+ | Strategy | Implementation |
1108
+ |----------|---------------|
1109
+ | Const widgets | Mark all stateless leaf widgets `const` |
1110
+ | List virtualization | Always use `ListView.builder`, never `Column` for dynamic lists |
1111
+ | Image caching | `cached_network_image` with disk cache |
1112
+ | State granularity | Use `select()` on providers to prevent unnecessary rebuilds |
1113
+ | Build profiling | `flutter run --profile` + DevTools |
1114
+ | Isolates | Offload heavy computation with `compute()` |
1115
+ | App startup | Defer non-critical initialization after first frame |
1116
+
1117
+ ---
1118
+
1119
+ ## Localization
1120
+
1121
+ All user-visible text must be in Spanish. Code, logs, comments, and git commits stay in English (same rule as frontend and backend standards).
1122
+
1123
+ ```yaml
1124
+ # pubspec.yaml
1125
+ flutter:
1126
+ generate: true
1127
+
1128
+ # l10n.yaml
1129
+ arb-dir: lib/l10n
1130
+ template-arb-file: app_en.arb
1131
+ output-localization-file: app_localizations.dart
1132
+ ```
1133
+
1134
+ ```json
1135
+ // lib/l10n/app_es.arb
1136
+ {
1137
+ "@@locale": "es",
1138
+ "cartTitle": "Carrito",
1139
+ "cartEmpty": "Tu carrito está vacío",
1140
+ "addToCart": "Agregar al carrito",
1141
+ "removeFromCart": "Eliminar del carrito",
1142
+ "checkout": "Finalizar compra",
1143
+ "errorGeneric": "Ha ocurrido un error. Intenta de nuevo.",
1144
+ "errorNotFound": "El recurso solicitado no fue encontrado",
1145
+ "loading": "Cargando..."
1146
+ }
1147
+ ```
1148
+
1149
+ ---
1150
+
1151
+ ## Code Generation
1152
+
1153
+ Run after every change to annotated classes:
1154
+
1155
+ ```bash
1156
+ dart run build_runner build --delete-conflicting-outputs
1157
+ ```
1158
+
1159
+ Files generated automatically (never edit manually):
1160
+
1161
+ | Generator | Output |
1162
+ |-----------|--------|
1163
+ | `freezed` | `*.freezed.dart` |
1164
+ | `json_serializable` | `*.g.dart` (JSON) |
1165
+ | `riverpod_generator` | `*.g.dart` (providers) |
1166
+ | `injectable_generator` | `injection.config.dart` |
1167
+ | `drift_dev` | `*.g.dart` (DB) |
1168
+ | `flutter_gen` | `assets.gen.dart` |
1169
+
1170
+ Add all `*.g.dart` and `*.freezed.dart` to `.gitignore` only if the project enforces generation in CI. Otherwise commit them for faster cold builds.
1171
+
1172
+ ---
1173
+
1174
+ ## pubspec.yaml Reference
1175
+
1176
+ ```yaml
1177
+ name: your_app
1178
+ description: Enterprise Flutter app
1179
+
1180
+ environment:
1181
+ sdk: '>=3.6.0 <4.0.0'
1182
+ flutter: '>=3.27.0'
1183
+
1184
+ dependencies:
1185
+ flutter:
1186
+ sdk: flutter
1187
+ flutter_localizations:
1188
+ sdk: flutter
1189
+
1190
+ # Architecture & DI
1191
+ get_it: ^8.0.0
1192
+ injectable: ^2.5.0
1193
+ dartz: ^0.10.1
1194
+
1195
+ # State management
1196
+ flutter_riverpod: ^2.6.0
1197
+ riverpod_annotation: ^2.4.0
1198
+
1199
+ # Navigation
1200
+ go_router: ^14.0.0
1201
+
1202
+ # Network
1203
+ dio: ^5.7.0
1204
+ connectivity_plus: ^6.0.0
1205
+
1206
+ # Local storage
1207
+ drift: ^2.23.0
1208
+ sqlite3_flutter_libs: ^0.5.0
1209
+ flutter_secure_storage: ^9.2.0
1210
+ shared_preferences: ^2.3.0
1211
+
1212
+ # Models
1213
+ freezed_annotation: ^2.4.0
1214
+ json_annotation: ^4.9.0
1215
+
1216
+ # Validation
1217
+ formz: ^0.7.0
1218
+
1219
+ # UI
1220
+ cached_network_image: ^3.4.0
1221
+
1222
+ dev_dependencies:
1223
+ flutter_test:
1224
+ sdk: flutter
1225
+ build_runner: ^2.4.0
1226
+ freezed: ^2.5.0
1227
+ json_serializable: ^6.8.0
1228
+ riverpod_generator: ^2.4.0
1229
+ injectable_generator: ^2.6.0
1230
+ drift_dev: ^2.23.0
1231
+ flutter_gen_runner: ^5.7.0
1232
+ mocktail: ^1.0.4
1233
+ patrol: ^3.13.0
1234
+ very_good_analysis: ^6.0.0
1235
+
1236
+ flutter:
1237
+ uses-material-design: true
1238
+ generate: true
1239
+ fonts:
1240
+ - family: Inter
1241
+ fonts:
1242
+ - asset: assets/fonts/Inter_18pt-Light.ttf
1243
+ weight: 300
1244
+ - asset: assets/fonts/Inter_18pt-Regular.ttf
1245
+ weight: 400
1246
+ - asset: assets/fonts/Inter_18pt-Bold.ttf
1247
+ weight: 700
1248
+ assets:
1249
+ - assets/fonts/
1250
+ - assets/images/
1251
+ - assets/images/logos/
1252
+ ```
1253
+
1254
+ ---
1255
+
1256
+ ## Implementation Checklist
1257
+
1258
+ ### Project Setup
1259
+ - [ ] Flutter 3.27+ stable channel
1260
+ - [ ] `very_good_analysis` lint rules configured
1261
+ - [ ] `l10n.yaml` and `.arb` files created
1262
+ - [ ] `get_it` + `injectable` wired in `main.dart`
1263
+ - [ ] `ProviderScope` wrapping the entire app
1264
+ - [ ] GoRouter with auth redirect guard
1265
+ - [ ] `AppTheme.light` and `AppTheme.dark` applied
1266
+ - [ ] Inter font family assets loaded
1267
+ - [ ] Brand colors in `AppColors` matching `technical-preferences-ux.md`
1268
+ - [ ] `build_runner` script added to `Makefile` or `justfile`
1269
+
1270
+ ### Per Feature
1271
+ - [ ] Domain entities with `Freezed`
1272
+ - [ ] Value objects with `Either` return
1273
+ - [ ] Sealed failure class
1274
+ - [ ] Repository interface in domain
1275
+ - [ ] Use case(s) with `@injectable`
1276
+ - [ ] `AsyncNotifier` provider with `@riverpod`
1277
+ - [ ] Infrastructure model with `fromDomain` / `toDomain`
1278
+ - [ ] Remote datasource using `DioClient`
1279
+ - [ ] Local datasource using `Drift` or `SecureStorage`
1280
+ - [ ] Repository implementation with `@LazySingleton(as: IRepository)`
1281
+ - [ ] Page using `ConsumerWidget`
1282
+ - [ ] Widgets separated from pages
1283
+ - [ ] Unit tests for use cases and value objects
1284
+ - [ ] Widget tests for key interactions
1285
+
1286
+ ### Quality
1287
+ - [ ] `flutter analyze` returns zero warnings
1288
+ - [ ] All user-visible strings in Spanish `.arb` files
1289
+ - [ ] No hardcoded colors or text sizes (use theme tokens)
1290
+ - [ ] `const` constructors on all leaf widgets
1291
+ - [ ] `ListView.builder` for all dynamic lists
1292
+ - [ ] Sensitive data only in `flutter_secure_storage`