--- name: krow-mobile-architecture description: KROW mobile app Clean Architecture implementation including package structure, dependency rules, feature isolation, BLoC lifecycle management, session handling, and V2 REST API integration. Use this when architecting new mobile features, debugging state management issues, preventing prop drilling, managing BLoC disposal, implementing session stores, or setting up API repository patterns. Essential for maintaining architectural integrity across staff and client apps. --- # KROW Mobile Architecture This skill defines the authoritative mobile architecture for the KROW platform. All code must strictly adhere to these principles to prevent architectural degradation. ## When to Use This Skill - Architecting new mobile features - Debugging state management or BLoC lifecycle issues - Preventing prop drilling in UI code - Managing session state and authentication - Implementing V2 API repository patterns - Setting up feature modules and dependency injection - Understanding package boundaries and dependencies - Refactoring legacy code to Clean Architecture ## 1. High-Level Architecture KROW follows **Clean Architecture** in a **Melos Monorepo**. Dependencies flow **inward** toward the Domain. ``` ┌─────────────────────────────────────────────────────────┐ │ Apps (Entry Points) │ │ • apps/mobile/apps/client │ │ • apps/mobile/apps/staff │ │ Role: DI roots, navigation assembly, env config │ └─────────────────┬───────────────────────────────────────┘ │ depends on ┌─────────────────▼───────────────────────────────────────┐ │ Features (Vertical Slices) │ │ • apps/mobile/packages/features/client/* │ │ • apps/mobile/packages/features/staff/* │ │ Role: Pages, BLoCs, Use Cases, Feature Repositories │ └─────┬───────────────────────────────────────┬───────────┘ │ depends on │ depends on ┌─────▼────────────────┐ ┌───────▼───────────┐ │ Design System │ │ Core Localization│ │ • UI components │ │ • LocaleBloc │ │ • Theme/colors │ │ • Translations │ │ • Typography │ │ • ErrorTranslator│ └──────────────────────┘ └───────────────────┘ │ both depend on ┌─────────────────▼───────────────────────────────────────┐ │ Services (Interface Adapters) │ │ • core: API service, session management, device │ │ services, utilities, extensions, base classes │ └─────────────────┬───────────────────────────────────────┘ │ depends on ┌─────────────────▼───────────────────────────────────────┐ │ Domain (Stable Core) │ │ • Entities (data models with fromJson/toJson) │ │ • Enums (shared enumerations) │ │ • Failures (domain-specific errors) │ │ • Pure Dart only, zero Flutter dependencies │ └─────────────────────────────────────────────────────────┘ ``` **Critical Rule:** Dependencies point INWARD only. Domain knows nothing about the outer layers. ## 2. Package Structure & Responsibilities ### 2.1 Apps (`apps/mobile/apps/`) **Role:** Application entry points and DI roots **Responsibilities:** - Initialize Flutter Modular - Assemble features into navigation tree - Inject concrete implementations into features - Configure environment-specific settings (dev/stage/prod) - Initialize session management via `V2SessionService` **Structure:** ``` apps/mobile/apps/staff/ ├── lib/ │ ├── main.dart # Entry point, session initialization │ ├── app_module.dart # Root module, imports features │ ├── app_widget.dart # MaterialApp setup │ └── src/ │ ├── navigation/ # Typed navigators │ └── widgets/ # SessionListener wrapper └── pubspec.yaml ``` **RESTRICTION:** NO business logic. NO UI widgets (except App and Main). ### 2.2 Features (`apps/mobile/packages/features//`) **Role:** Vertical slices of user-facing functionality **Internal Structure:** ``` features/staff/profile/ ├── lib/ │ ├── src/ │ │ ├── domain/ │ │ │ ├── repositories/ # Repository interfaces │ │ │ │ └── profile_repository_interface.dart │ │ │ └── usecases/ # Application logic │ │ │ └── get_profile_usecase.dart │ │ ├── data/ │ │ │ └── repositories_impl/ # Repository concrete classes │ │ │ └── profile_repository_impl.dart │ │ └── presentation/ │ │ ├── blocs/ # State management │ │ │ └── profile_cubit.dart │ │ ├── pages/ # Screens (StatelessWidget preferred) │ │ │ └── profile_page.dart │ │ └── widgets/ # Reusable UI components │ │ └── profile_header.dart │ └── profile_feature.dart # Barrel file (public API only) └── pubspec.yaml ``` **Key Principles:** - **Presentation:** UI Pages and Widgets, BLoCs/Cubits for state - **Application:** Use Cases (business logic orchestration) - **Data:** Repository implementations using `ApiService` with `V2ApiEndpoints` - **Pages as StatelessWidget:** Move state to BLoCs for better performance and testability - **Feature-level domain is optional:** Only needed when the feature has business logic (use cases, validators). Simple features can have just `data/` + `presentation/`. **RESTRICTION:** Features MUST NOT import other features. Communication happens via: - Shared domain entities - Session stores (`StaffSessionStore`, `ClientSessionStore`) - Navigation via Modular ### 2.3 Domain (`apps/mobile/packages/domain`) **Role:** The stable, pure heart of the system **Responsibilities:** - Define **Entities** (data models with `fromJson`/`toJson` for V2 API serialization) - Define **Enums** (shared enumerations in `entities/enums/`) - Define **Failures** (domain-specific error types) **Structure:** ``` domain/ ├── lib/ │ └── src/ │ ├── entities/ │ │ ├── user.dart │ │ ├── staff.dart │ │ ├── shift.dart │ │ └── enums/ │ │ ├── staff_status.dart │ │ └── order_type.dart │ ├── failures/ │ │ ├── failure.dart # Base class │ │ ├── auth_failure.dart │ │ └── network_failure.dart │ └── core/ │ └── services/api_services/ │ └── base_api_service.dart └── pubspec.yaml ``` **Example Entity:** ```dart import 'package:equatable/equatable.dart'; class Staff extends Equatable { final String id; final String name; final String email; final StaffStatus status; const Staff({ required this.id, required this.name, required this.email, required this.status, }); factory Staff.fromJson(Map json) { return Staff( id: json['id'] as String, name: json['name'] as String, email: json['email'] as String, status: StaffStatus.values.byName(json['status'] as String), ); } Map toJson() => { 'id': id, 'name': name, 'email': email, 'status': status.name, }; @override List get props => [id, name, email, status]; } ``` **RESTRICTION:** - NO Flutter dependencies (no `import 'package:flutter/material.dart'`) - Only `equatable` for value equality - Pure Dart only - `fromJson`/`toJson` live directly on entities (no separate DTOs or adapters) ### 2.4 Core (`apps/mobile/packages/core`) **Role:** Cross-cutting concerns, API infrastructure, session management, device services, and utilities **Responsibilities:** - `ApiService` — HTTP client wrapper around Dio with consistent response/error handling - `V2ApiEndpoints` — All V2 REST API endpoint constants - `DioClient` — Pre-configured Dio with `AuthInterceptor` and `IdempotencyInterceptor` - `AuthInterceptor` — Automatically attaches Firebase Auth ID token to requests - `IdempotencyInterceptor` — Adds `Idempotency-Key` header to POST/PUT/DELETE requests - `ApiErrorHandler` mixin — Maps API errors to domain failures - `SessionHandlerMixin` — Handles auth state, token refresh, role validation - `V2SessionService` — Manages session lifecycle, replaces legacy DataConnectService - Session stores (`StaffSessionStore`, `ClientSessionStore`) - Device services (camera, gallery, location, notifications, storage, etc.) - Extension methods (`NavigationExtensions`, `ListExtensions`, etc.) - Base classes (`UseCase`, `Failure`, `BlocErrorHandler`) - Logger configuration - `AppConfig` — Environment-specific configuration (API base URLs, keys) **Structure:** ``` core/ ├── lib/ │ ├── core.dart # Barrel exports │ └── src/ │ ├── config/ │ │ ├── app_config.dart # Env-specific config (V2_API_BASE_URL, etc.) │ │ └── app_environment.dart │ ├── services/ │ │ ├── api_service/ │ │ │ ├── api_service.dart # ApiService (get/post/put/patch/delete) │ │ │ ├── dio_client.dart # Pre-configured Dio │ │ │ ├── inspectors/ │ │ │ │ ├── auth_interceptor.dart │ │ │ │ └── idempotency_interceptor.dart │ │ │ ├── mixins/ │ │ │ │ ├── api_error_handler.dart │ │ │ │ └── session_handler_mixin.dart │ │ │ └── core_api_services/ │ │ │ ├── v2_api_endpoints.dart │ │ │ ├── core_api_endpoints.dart │ │ │ ├── file_upload/ │ │ │ ├── signed_url/ │ │ │ ├── llm/ │ │ │ ├── verification/ │ │ │ └── rapid_order/ │ │ ├── session/ │ │ │ ├── v2_session_service.dart │ │ │ ├── staff_session_store.dart │ │ │ └── client_session_store.dart │ │ └── device/ │ │ ├── camera/ │ │ ├── gallery/ │ │ ├── location/ │ │ ├── notification/ │ │ ├── storage/ │ │ └── background_task/ │ ├── presentation/ │ │ ├── mixins/ │ │ │ └── bloc_error_handler.dart │ │ └── observers/ │ │ └── core_bloc_observer.dart │ ├── routing/ │ │ └── routing.dart │ ├── domain/ │ │ ├── arguments/ │ │ └── usecases/ │ └── utils/ │ ├── date_time_utils.dart │ ├── geo_utils.dart │ └── time_utils.dart └── pubspec.yaml ``` **RESTRICTION:** - NO feature-specific logic - Core services are domain-neutral and reusable - All V2 API access goes through `ApiService` — never use raw Dio directly in features ### 2.5 Design System (`apps/mobile/packages/design_system`) **Role:** Visual language and component library **Responsibilities:** - Theme definitions (`UiColors`, `UiTypography`) - UI constants (`spacingL`, `radiusM`, etc.) - Shared widgets (if reused across multiple features) - Assets (icons, images, fonts) **Structure:** ``` design_system/ ├── lib/ │ └── src/ │ ├── ui_colors.dart │ ├── ui_typography.dart │ ├── ui_icons.dart │ ├── ui_constants.dart │ ├── ui_theme.dart # ThemeData factory │ └── widgets/ # Shared UI components │ └── custom_button.dart └── assets/ ├── icons/ └── images/ ``` **RESTRICTION:** - Dumb widgets ONLY (no state management) - NO business logic - Colors and typography are IMMUTABLE (no feature can override) ### 2.6 Core Localization (`apps/mobile/packages/core_localization`) **Role:** Centralized i18n management **Responsibilities:** - Define all user-facing strings in `l10n/` - Provide `LocaleBloc` for locale state management - Export `TranslationProvider` for `context.strings` access - Map domain failures to localized error messages via `ErrorTranslator` **String Definition:** - Strings are defined in `packages/core_localization/lib/src/l10n/en.i18n.json` (English) and `es.i18n.json` (Spanish) - Both files MUST be updated together when adding/modifying strings - Generated output: `strings.g.dart`, `strings_en.g.dart`, `strings_es.g.dart` - Regenerate with: `cd packages/core_localization && dart run slang` **Feature Integration:** ```dart // CORRECT: Access via Slang's global `t` accessor import 'package:core_localization/core_localization.dart'; Text(t.client_create_order.review.invalid_arguments) Text(t.errors.order.creation_failed) // FORBIDDEN: Hardcoded user-facing strings Text('Invalid review arguments') // Must use localized key Text('Order created!') // Must use localized key ``` **RESTRICTION:** ALL user-facing strings in the presentation layer (Text widgets, SnackBars, AppBar titles, hints, labels, error messages, dialogs) MUST use localized keys via `t.
.`. No hardcoded English or Spanish strings. **BLoC Error Flow:** ```dart // BLoCs emit domain failures (not strings) emit(AuthError(InvalidCredentialsFailure())); // UI translates failures to localized messages final message = ErrorTranslator.translate(failure, context.strings); ``` **App Setup:** ```dart // App imports LocalizationModule class AppModule extends Module { @override List get imports => [LocalizationModule()]; } // Wrap app with providers BlocProvider( create: (_) => Modular.get(), child: TranslationProvider( child: MaterialApp.router(...), ), ) ``` ## 3. Dependency Direction Rules 1. **Domain Independence:** `domain` knows NOTHING about outer layers - Defines *what* needs to be done, not *how* - Pure Dart, zero Flutter dependencies - Stable contracts that rarely change - Entities include `fromJson`/`toJson` for practical V2 API serialization 2. **UI Agnosticism:** Features depend on `design_system` for UI and `domain` for logic - Features do NOT know about HTTP/Dio details - Backend changes don't affect feature implementation 3. **Data Isolation:** Feature `data/` layer depends on `core` for API access and `domain` for entities - RepoImpl uses `ApiService` with `V2ApiEndpoints` - Maps JSON responses to domain entities via `Entity.fromJson()` - Does NOT know about UI **Dependency Flow:** ``` Apps → Features → Design System → Core Localization → Core → Domain ``` ## 4. V2 API Service & Session Management ### 4.1 ApiService **Location:** `apps/mobile/packages/core/lib/src/services/api_service/api_service.dart` **Responsibilities:** - Wraps Dio HTTP methods (GET, POST, PUT, PATCH, DELETE) - Consistent response parsing via `ApiResponse` - Consistent error handling (maps `DioException` to `ApiResponse` with V2 error envelope) **Key Usage:** ```dart final ApiService apiService; // GET request final response = await apiService.get( V2ApiEndpoints.staffDashboard, params: {'date': '2026-01-15'}, ); // POST request final response = await apiService.post( V2ApiEndpoints.staffClockIn, data: {'shiftId': shiftId, 'latitude': lat, 'longitude': lng}, ); ``` ### 4.2 DioClient & Interceptors **Location:** `apps/mobile/packages/core/lib/src/services/api_service/dio_client.dart` **Pre-configured with:** - `AuthInterceptor` — Automatically attaches Firebase Auth ID token as `Bearer` token - `IdempotencyInterceptor` — Adds `Idempotency-Key` (UUID v4) to POST/PUT/DELETE requests - `LogInterceptor` — Logs request/response bodies for debugging ### 4.3 V2SessionService **Location:** `apps/mobile/packages/core/lib/src/services/session/v2_session_service.dart` **Responsibilities:** - Manages session lifecycle (initialize, refresh, invalidate) - Fetches session data from V2 API on auth state change - Populates session stores with user/role data - Provides session state stream for `SessionListener` **Key Method:** ```dart // Call once on app startup V2SessionService.instance.initializeAuthListener( allowedRoles: ['STAFF', 'BOTH'], // or ['CLIENT', 'BUSINESS', 'BOTH'] ); ``` ### 4.4 Session Listener Widget **Location:** `apps/mobile/apps//lib/src/widgets/session_listener.dart` **Responsibilities:** - Wraps entire app to listen to session state changes - Shows user-friendly dialogs for session expiration/errors - Handles navigation on auth state changes **Usage:** ```dart // main.dart runApp( SessionListener( // Critical wrapper child: ModularApp(module: AppModule(), child: AppWidget()), ), ); ``` ### 4.5 Repository Pattern with V2 API **Step 1:** Define interface in feature domain: ```dart // features/staff/profile/lib/src/domain/repositories/ abstract interface class ProfileRepositoryInterface { Future getProfile(String id); } ``` **Step 2:** Implement using `ApiService` with `V2ApiEndpoints`: ```dart // features/staff/profile/lib/src/data/repositories_impl/ class ProfileRepositoryImpl implements ProfileRepositoryInterface { final ApiService _apiService; ProfileRepositoryImpl({required ApiService apiService}) : _apiService = apiService; @override Future getProfile(String id) async { final response = await _apiService.get( V2ApiEndpoints.staffSession, params: {'staffId': id}, ); return Staff.fromJson(response.data as Map); } } ``` **Benefits of `ApiService` + interceptors:** - AuthInterceptor auto-attaches Firebase Auth token - IdempotencyInterceptor prevents duplicate writes - Consistent error handling via `ApiResponse` - No manual token management in features ### 4.6 Session Store Pattern After successful auth, populate session stores: **Staff App:** ```dart StaffSessionStore.instance.setSession( StaffSession( user: user, staff: staff, ownerId: ownerId, ), ); ``` **Client App:** ```dart ClientSessionStore.instance.setSession( ClientSession( user: user, business: business, ), ); ``` **Lazy Loading:** If session is null, fetch from backend and update: ```dart final session = StaffSessionStore.instance.session; if (session?.staff == null) { final response = await apiService.get(V2ApiEndpoints.staffSession); final staff = Staff.fromJson(response.data as Map); StaffSessionStore.instance.setSession( session!.copyWith(staff: staff), ); } ``` ## 5. Feature Isolation & Communication ### Zero Direct Imports ```dart // FORBIDDEN import 'package:staff_profile/staff_profile.dart'; // in another feature // ALLOWED import 'package:krow_domain/krow_domain.dart'; // shared domain import 'package:krow_core/krow_core.dart'; // shared utilities + API import 'package:design_system/design_system.dart'; // shared UI ``` ### Navigation: Typed Navigators with Safe Extensions **Safe Navigation Extensions** (from `core` package): ```dart extension NavigationExtensions on IModularNavigator { /// Safely navigate with fallback to home Future safeNavigate(String route) async { try { await navigate(route); } catch (e) { await navigate('/home'); // Fallback } } /// Safely push with fallback to home Future safePush(String route) async { try { return await pushNamed(route); } catch (e) { await navigate('/home'); return null; } } /// Safely pop with guard against empty stack void popSafe() { if (canPop()) { pop(); } else { navigate('/home'); } } } ``` **Typed Navigators:** ```dart // apps/mobile/apps/staff/lib/src/navigation/staff_navigator.dart extension StaffNavigator on IModularNavigator { Future toStaffHome() => safeNavigate(StaffPaths.home); Future toShiftDetails(String shiftId) => safePush('${StaffPaths.shifts}/$shiftId'); Future toProfileEdit() => safePush(StaffPaths.profileEdit); } ``` **Usage in Features:** ```dart // CORRECT Modular.to.toStaffHome(); Modular.to.toShiftDetails(shiftId: '123'); Modular.to.popSafe(); // AVOID Modular.to.navigate('/profile'); // No safety Navigator.push(...); // No Modular integration ``` ### Data Sharing Patterns Features don't share state directly. Use: 1. **Domain Repositories:** Centralized data sources via `ApiService` 2. **Session Stores:** `StaffSessionStore`, `ClientSessionStore` for app-wide context 3. **Event Streams:** If needed, via `V2SessionService` streams 4. **Navigation Arguments:** Pass IDs, not full objects ## 6. App-Specific Session Management ### Staff App ```dart // main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); V2SessionService.instance.initializeAuthListener( allowedRoles: ['STAFF', 'BOTH'], ); runApp( SessionListener( child: ModularApp(module: StaffAppModule(), child: StaffApp()), ), ); } ``` **Session Store:** `StaffSessionStore` - Fields: `user`, `staff`, `ownerId` - Lazy load: fetch from `V2ApiEndpoints.staffSession` if staff is null **Navigation:** - Authenticated -> `Modular.to.toStaffHome()` - Unauthenticated -> `Modular.to.toInitialPage()` ### Client App ```dart // main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); V2SessionService.instance.initializeAuthListener( allowedRoles: ['CLIENT', 'BUSINESS', 'BOTH'], ); runApp( SessionListener( child: ModularApp(module: ClientAppModule(), child: ClientApp()), ), ); } ``` **Session Store:** `ClientSessionStore` - Fields: `user`, `business` - Lazy load: fetch from `V2ApiEndpoints.clientSession` if business is null **Navigation:** - Authenticated -> `Modular.to.toClientHome()` - Unauthenticated -> `Modular.to.toInitialPage()` ## 7. V2 API Repository Pattern **Problem:** Without a consistent pattern, each feature handles HTTP differently. **Solution:** Feature RepoImpl uses `ApiService` with `V2ApiEndpoints`, returning domain entities via `Entity.fromJson()`. ### Structure Repository implementations live in the feature package: ``` features/staff/profile/ ├── lib/src/ │ ├── domain/ │ │ └── repositories/ │ │ └── profile_repository_interface.dart # Interface │ ├── data/ │ │ └── repositories_impl/ │ │ └── profile_repository_impl.dart # Implementation │ └── presentation/ │ └── blocs/ │ └── profile_cubit.dart ``` ### Repository Interface ```dart // profile_repository_interface.dart abstract interface class ProfileRepositoryInterface { Future getProfile(); Future updatePersonalInfo(Map data); Future> getProfileSections(); } ``` ### Repository Implementation ```dart // profile_repository_impl.dart class ProfileRepositoryImpl implements ProfileRepositoryInterface { final ApiService _apiService; ProfileRepositoryImpl({required ApiService apiService}) : _apiService = apiService; @override Future getProfile() async { final response = await _apiService.get(V2ApiEndpoints.staffSession); final data = response.data as Map; return Staff.fromJson(data['staff'] as Map); } @override Future updatePersonalInfo(Map data) async { await _apiService.put( V2ApiEndpoints.staffPersonalInfo, data: data, ); } @override Future> getProfileSections() async { final response = await _apiService.get(V2ApiEndpoints.staffProfileSections); final list = response.data['sections'] as List; return list .map((e) => ProfileSection.fromJson(e as Map)) .toList(); } } ``` ### Feature Module Integration ```dart // profile_module.dart class ProfileModule extends Module { @override void binds(Injector i) { i.addLazySingleton( () => ProfileRepositoryImpl(apiService: i.get()), ); i.addLazySingleton( () => GetProfileUseCase( repository: i.get(), ), ); i.addLazySingleton( () => ProfileCubit( getProfileUseCase: i.get(), ), ); } } ``` ### BLoC Usage ```dart class ProfileCubit extends Cubit with BlocErrorHandler { final GetProfileUseCase _getProfileUseCase; Future loadProfile() async { emit(state.copyWith(status: ProfileStatus.loading)); await handleError( emit: emit, action: () async { final profile = await _getProfileUseCase(); emit(state.copyWith(status: ProfileStatus.loaded, profile: profile)); }, onError: (errorKey) => state.copyWith(status: ProfileStatus.error), ); } } ``` ### Benefits - **No Duplication** — Endpoint constants defined once in `V2ApiEndpoints` - **Consistent Auth** — `AuthInterceptor` handles token attachment automatically - **Idempotent Writes** — `IdempotencyInterceptor` prevents duplicate mutations - **Domain Purity** — Entities use `fromJson`/`toJson` directly, no mapping layers - **Testability** — Mock `ApiService` to test RepoImpl in isolation - **Scalability** — Add new endpoints to `V2ApiEndpoints`, implement in feature RepoImpl ## 8. Avoiding Prop Drilling: Direct BLoC Access ### The Problem Passing data through intermediate widgets creates maintenance burden: ```dart // BAD: Prop drilling ProfilePage(status: status) -> ProfileHeader(status: status) -> ProfileLevelBadge(status: status) // Only widget that needs it ``` ### The Solution: BlocBuilder in Leaf Widgets ```dart // GOOD: Direct BLoC access class ProfileLevelBadge extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state.profile == null) return const SizedBox.shrink(); final level = _mapStatusToLevel(state.profile!.status); return LevelBadgeUI(level: level); }, ); } } ``` ### Guidelines 1. **Leaf Widgets Access BLoC:** Widgets needing specific data should use `BlocBuilder` 2. **Container Widgets Stay Simple:** Parent widgets only manage layout 3. **No Unnecessary Props:** Don't pass data to intermediate widgets 4. **Single Responsibility:** Each widget has one reason to exist **Decision Tree:** ``` Does this widget need data? ├─ YES, leaf widget -> Use BlocBuilder ├─ YES, container -> Use BlocBuilder in child └─ NO -> Don't add prop ``` ## 9. BLoC Lifecycle & State Emission Safety ### The Problem: StateError After Dispose When async operations complete after BLoC is closed: ``` StateError: Cannot emit new states after calling close ``` **Root Causes:** 1. Transient BLoCs created with `BlocProvider(create:)` -> disposed prematurely 2. Multiple BlocProviders disposing same singleton 3. User navigates away during async operation ### The Solution: Singleton BLoCs + Safe Emit #### Step 1: Register as Singleton ```dart // GOOD: Singleton registration i.addLazySingleton( () => ProfileCubit(useCase1, useCase2), ); // BAD: Creates new instance each time i.add(ProfileCubit.new); ``` #### Step 2: Use BlocProvider.value() ```dart // GOOD: Reuse singleton final cubit = Modular.get(); BlocProvider.value( value: cubit, child: MyWidget(), ) // BAD: Creates duplicate BlocProvider( create: (_) => Modular.get(), child: MyWidget(), ) ``` #### Step 3: Safe Emit with BlocErrorHandler **Location:** `apps/mobile/packages/core/lib/src/presentation/mixins/bloc_error_handler.dart` ```dart mixin BlocErrorHandler on Cubit { void _safeEmit(void Function(S) emit, S state) { try { emit(state); } on StateError catch (e) { developer.log( 'Could not emit state: ${e.message}. Bloc may have been disposed.', name: runtimeType.toString(), ); } } } ``` **Usage:** ```dart class ProfileCubit extends Cubit with BlocErrorHandler { Future loadProfile() async { emit(state.copyWith(status: ProfileStatus.loading)); await handleError( emit: emit, action: () async { final profile = await getProfile(); emit(state.copyWith(status: ProfileStatus.loaded, profile: profile)); // Safe even if BLoC disposed }, onError: (errorKey) => state.copyWith(status: ProfileStatus.error), ); } } ``` ### Pattern Summary | Pattern | When to Use | Risk | |---------|------------|------| | Singleton + BlocProvider.value() | Long-lived features | Low | | Transient + BlocProvider(create:) | Temporary widgets | Medium | | Direct BlocBuilder | Leaf widgets | Low | ## 10. Anti-Patterns to Avoid - **Feature imports feature** ```dart import 'package:staff_profile/staff_profile.dart'; // in another feature ``` - **Business logic in BLoC** ```dart on((event, emit) { if (event.email.isEmpty) { // Use case responsibility emit(AuthError('Email required')); } }); ``` - **Direct HTTP/Dio in features (use ApiService)** ```dart final response = await Dio().get('https://api.example.com/staff'); // Use ApiService ``` - **Importing krow_data_connect (deprecated package)** ```dart import 'package:krow_data_connect/krow_data_connect.dart'; // Use krow_core instead ``` - **Global state variables** ```dart User? currentUser; // Use SessionStore ``` - **Direct Navigator.push** ```dart Navigator.push(context, MaterialPageRoute(...)); // Use Modular ``` - **Hardcoded navigation** ```dart Modular.to.navigate('/profile'); // Use safe extensions ``` - **Hardcoded user-facing strings** ```dart Text('Order created successfully!'); // Use t.section.key from core_localization ``` ## Summary The architecture enforces: - **Clean Architecture** with strict layer boundaries - **Feature Isolation** via zero cross-feature imports - **V2 REST API** integration via `ApiService`, `V2ApiEndpoints`, and interceptors - **Session Management** via `V2SessionService`, session stores, and `SessionListener` - **Repository Pattern** with feature-local RepoImpl using `ApiService` - **BLoC Lifecycle** safety with singletons and safe emit - **Navigation Safety** with typed navigators and fallbacks When implementing features: 1. Follow package structure strictly 2. Use `ApiService` with `V2ApiEndpoints` for all backend access 3. Domain entities use `fromJson`/`toJson` for V2 API serialization 4. RepoImpl lives in the feature `data/` layer, not a shared package 5. Register BLoCs as singletons with `.value()` 6. Use safe navigation extensions 7. Avoid prop drilling with direct BLoC access 8. Keep domain pure and stable Architecture is not negotiable. When in doubt, refer to existing well-structured features or ask for clarification.