# KROW Architecture Principles This document is the **AUTHORITATIVE** source of truth for the KROW engineering architecture. All agents and engineers must adhere strictly to these principles. Deviations are interpreted as errors. ## 1. High-Level Architecture The KROW platform follows a strict **Clean Architecture** implementation within a **Melos Monorepo**. Dependencies flow **inwards** towards the Domain. ```mermaid graph TD subgraph "Apps (Entry Points)" ClientApp["apps/mobile/apps/client"] StaffApp["apps/mobile/apps/staff"] end subgraph "Features" ClientFeatures["apps/mobile/packages/features/client/*"] StaffFeatures["apps/mobile/packages/features/staff/*"] end subgraph "Services" DataConnect["apps/mobile/packages/data_connect"] DesignSystem["apps/mobile/packages/design_system"] CoreLocalization["apps/mobile/packages/core_localization"] end subgraph "Core Domain" Domain["apps/mobile/packages/domain"] Core["apps/mobile/packages/core"] end %% Dependency Flow ClientApp --> ClientFeatures & DataConnect & CoreLocalization StaffApp --> StaffFeatures & DataConnect & CoreLocalization ClientFeatures & StaffFeatures --> Domain ClientFeatures & StaffFeatures --> DesignSystem ClientFeatures & StaffFeatures --> CoreLocalization ClientFeatures & StaffFeatures --> Core DataConnect --> Domain DataConnect --> Core DesignSystem --> Core CoreLocalization --> Core Domain --> Core %% Strict Barriers linkStyle default stroke-width:2px,fill:none,stroke:gray ``` ## 2. Repository Structure & Package Roles ### 2.1 Apps (`apps/mobile/apps/`) - **Role**: Application entry points and Dependency Injection (DI) roots. - **Responsibilities**: - Initialize Flutter Modular. - Assemble features into a navigation tree. - Inject concrete implementations (from `data_connect`) into Feature packages. - Configure environment-specific settings. - **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**: - `domain/`: Feature-specific Use Cases(always extend the apps/mobile/packages/core/lib/src/domain/usecases/usecase.dart abstract clas) and Repository Interfaces. - `data/`: Repository Implementations. - `presentation/`: - Pages, BLoCs, Widgets. - For performance make the pages as `StatelessWidget` and move the state management to the BLoC (always use a BlocProvider when providing the BLoC to the widget tree) or `StatefulWidget` to an external separate widget file. - **Responsibilities**: - **Presentation**: UI Pages, Modular Routes. - **State Management**: BLoCs / Cubits. - **Application Logic**: Use Cases. - **RESTRICTION**: Features MUST NOT import other features. Communication happens via shared domain events. ### 2.3 Domain (`apps/mobile/packages/domain`) - **Role**: The stable heart of the system. Pure Dart. - **Responsibilities**: - **Entities**: Immutable data models (Data Classes). - **Failures**: Domain-specific error types. - **RESTRICTION**: NO Flutter dependencies. NO `json_annotation`. NO package dependencies (except `equatable`). ### 2.4 Data Connect (`apps/mobile/packages/data_connect`) - **Role**: Interface Adapter for Backend Access (Datasource Layer). - **Responsibilities**: - **Connectors**: Centralized repository implementations for each backend connector (see `03-data-connect-connectors-pattern.md`) - One connector per backend connector domain (staff, order, user, etc.) - Repository interfaces and use cases defined at domain level - Repository implementations query backend and map responses - Implement Firebase Data Connect connector and service layer - Map Domain Entities to/from Data Connect generated code - Handle Firebase exceptions and map to domain failures - Provide centralized `DataConnectService` with session management - **RESTRICTION**: - NO feature-specific logic. Connectors are domain-neutral and reusable. - All queries must follow Clean Architecture (domain → data layers) - See `03-data-connect-connectors-pattern.md` for detailed pattern documentation ### 2.5 Design System (`apps/mobile/packages/design_system`) - **Role**: Visual language and component library. - **Responsibilities**: - UI components if needed. But mostly try to modify the theme file (apps/mobile/packages/design_system/lib/src/ui_theme.dart) so we can directly use the theme in the app, to use the default material widgets. - If not possible, and if that specific widget is used in multiple features, then try to create a shared widget in the `apps/mobile/packages/design_system/widgets`. - Theme definitions (Colors, Typography). - Assets (Icons, Images). - More details on how to use this package is available in the `apps/mobile/docs/03-design-system-usage.md`. - **RESTRICTION**: - CANNOT change colours or typography. - Dumb widgets only. NO business logic. NO state management (Bloc). - More details on how to use this package is available in the `apps/mobile/docs/03-design-system-usage.md`. ### 2.6 Core Localization (`apps/mobile/packages/core_localization`) - **Role**: Centralized language and localization management. - **Responsibilities**: - Define all user-facing strings in `l10n/` with i18n tooling support - Provide `LocaleBloc` for reactive locale state management - Export `TranslationProvider` for BuildContext-based string access - Map domain failures to user-friendly localized error messages via `ErrorTranslator` - **Feature Integration**: - Features access strings via `context.strings.` in presentation layer - BLoCs don't depend on localization; they emit domain failures - Error translation happens in UI layer (pages/widgets) - **App Integration**: - Apps import `LocalizationModule()` in their module imports - Apps wrap the material app with `BlocProvider()` and `TranslationProvider` - Apps initialize `MaterialApp` with locale from `LocaleState` ### 2.7 Core (`apps/mobile/packages/core`) - **Role**: Cross-cutting concerns. - **Responsibilities**: - Extension methods. - Logger configuration. - Base classes for Use Cases or Result types (functional error handling). ## 3. Dependency Direction & Boundaries 1. **Domain Independence**: `apps/mobile/packages/domain` knows NOTHING about the outer world. It defines *what* needs to be done, not *how*. 2. **UI Agnosticism**: `apps/mobile/packages/features` depends on `apps/mobile/packages/design_system` for looks and `apps/mobile/packages/domain` for logic. It does NOT know about Firebase. 3. **Data Isolation**: `apps/mobile/packages/data_connect` depends on `apps/mobile/packages/domain` to know what interfaces to implement. It does NOT know about the UI. ## 4. Data Connect Service & Session Management All backend access is unified through `DataConnectService` with integrated session management: ### 4.1 Session Handler Mixin - **Location**: `apps/mobile/packages/data_connect/lib/src/services/mixins/session_handler_mixin.dart` - **Responsibilities**: - Automatic token refresh (triggered when token <5 minutes to expiry) - Firebase auth state listening - Role-based access validation - Session state stream emissions - 3-attempt retry logic with exponential backoff on token validation failure - **Key Method**: `initializeAuthListener(allowedRoles: [...])` - call once on app startup ### 4.2 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 - **Pattern**: `SessionListener(child: AppWidget())` ### 4.3 Repository Pattern with Data Connect 1. **Interface First**: Define `abstract interface class RepositoryInterface` in feature domain layer. 2. **Implementation**: Use `_service.run()` wrapper that automatically: - Validates user is authenticated (if required) - Ensures token is valid and refreshes if needed - Executes the Data Connect query - Handles exceptions and maps to domain failures 3. **Session Store Population**: On successful auth, session stores are populated: - Staff: `StaffSessionStore.instance.setSession(StaffSession(...))` - Client: `ClientSessionStore.instance.setSession(ClientSession(...))` 4. **Lazy Loading**: If session is null, fetch data via `getStaffById()` or `getBusinessById()` and update store. ## 5. Feature Isolation & Cross-Feature Communication - **Zero Direct Imports**: `import 'package:feature_a/...'` is FORBIDDEN inside `package:feature_b`. - Exception: Shared packages like `domain`, `core`, and `design_system` are always accessible. - **Navigation**: Use named routes via Flutter Modular: - **Pattern**: `Modular.to.navigate('route_name')` - **Configuration**: Routes defined in `module.dart` files; constants in `paths.dart` - **Data Sharing**: Features do not share state directly. Shared data accessed through: - **Domain Repositories**: Centralized data sources (e.g., `AuthRepository`) - **Session Stores**: `StaffSessionStore` and `ClientSessionStore` for app-wide user context - **Event Streams**: If needed, via `DataConnectService` streams for reactive updates ## 6. App-Specific Session Management Each app (`staff` and `client`) has different role requirements and session patterns: ### 6.1 Staff App Session - **Location**: `apps/mobile/apps/staff/lib/main.dart` - **Initialization**: `DataConnectService.instance.initializeAuthListener(allowedRoles: ['STAFF', 'BOTH'])` - **Session Store**: `StaffSessionStore` with `StaffSession(user: User, staff: Staff?, ownerId: String?)` - **Lazy Loading**: `getStaffName()` fetches via `getStaffById()` if session null - **Navigation**: On auth → `Modular.to.toStaffHome()`, on unauth → `Modular.to.toInitialPage()` ### 6.2 Client App Session - **Location**: `apps/mobile/apps/client/lib/main.dart` - **Initialization**: `DataConnectService.instance.initializeAuthListener(allowedRoles: ['CLIENT', 'BUSINESS', 'BOTH'])` - **Session Store**: `ClientSessionStore` with `ClientSession(user: User, business: ClientBusinessSession?)` - **Lazy Loading**: `getUserSessionData()` fetches via `getBusinessById()` if session null - **Navigation**: On auth → `Modular.to.toClientHome()`, on unauth → `Modular.to.toInitialPage()` ## 7. Data Connect Connectors Pattern See **`03-data-connect-connectors-pattern.md`** for comprehensive documentation on: - How connector repositories work - How to add queries to existing connectors - How to create new connectors - Integration patterns with features - Benefits and anti-patterns **Quick Reference**: - All backend queries centralized in `apps/mobile/packages/data_connect/lib/src/connectors/` - One connector per backend connector domain (staff, order, user, etc.) - Each connector follows Clean Architecture (domain interfaces + data implementations) - Features use connector repositories through dependency injection - Results in zero query duplication and single source of truth ## 8. Prop Drilling Prevention & Direct BLoC Access ### 8.1 The Problem: Prop Drilling Passing data through intermediate widgets creates maintenance headaches: - Every intermediate widget must accept and forward props - Changes to data structure ripple through multiple widget constructors - Reduces code clarity and increases cognitive load **Anti-Pattern Example**: ```dart // ❌ BAD: Drilling status through 3 levels ProfilePage(status: status) → ProfileHeader(status: status) → ProfileLevelBadge(status: status) // Only widget that needs it! ``` ### 8.2 The Solution: Direct BLoC Access with BlocBuilder Use `BlocBuilder` to access BLoC state directly in leaf widgets: **Correct Pattern**: ```dart // ✅ GOOD: ProfileLevelBadge accesses ProfileCubit directly class ProfileLevelBadge extends StatelessWidget { const ProfileLevelBadge({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { final Staff? profile = state.profile; if (profile == null) return const SizedBox.shrink(); final level = _mapStatusToLevel(profile.status); return LevelBadgeUI(level: level); }, ); } } ``` ### 8.3 Guidelines for Avoiding Prop Drilling 1. **Leaf Widgets Get Data from BLoC**: Widgets that need specific data should access it directly via BlocBuilder 2. **Container Widgets Stay Simple**: Parent widgets like `ProfileHeader` only manage layout and positioning 3. **No Unnecessary Props**: Don't pass data to intermediate widgets unless they need it for UI construction 4. **Single Responsibility**: Each widget should have one reason to exist **Decision Tree**: ``` Does this widget need data? ├─ YES, and it's a leaf widget → Use BlocBuilder ├─ YES, and it's a container → Use BlocBuilder in child, not parent └─ NO → Don't add prop to constructor ``` ## 9. BLoC Lifecycle & State Emission Safety ### 9.1 The Problem: StateError After Dispose When async operations complete after a BLoC is closed, attempting to emit state causes: ``` StateError: Cannot emit new states after calling close ``` **Root Causes**: 1. **Transient BLoCs**: `BlocProvider(create:)` creates new instance on every rebuild → disposed prematurely 2. **Singleton Disposal**: Multiple BlocProviders disposing same singleton instance 3. **Navigation During Async**: User navigates away while `loadProfile()` is still running ### 9.2 The Solution: Singleton BLoCs + Error Handler Defensive Wrapping #### Step 1: Register as Singleton ```dart // ✅ GOOD: ProfileCubit as singleton i.addSingleton( () => ProfileCubit(useCase1, useCase2), ); // ❌ BAD: Creates new instance each time i.add(ProfileCubit.new); ``` #### Step 2: Use BlocProvider.value() for Singletons ```dart // ✅ GOOD: Use singleton instance ProfileCubit cubit = Modular.get(); BlocProvider.value( value: cubit, // Reuse same instance child: MyWidget(), ) // ❌ BAD: Creates duplicate instance BlocProvider( create: (_) => Modular.get(), // Wrong! child: MyWidget(), ) ``` #### Step 3: Defensive Error Handling in BlocErrorHandler Mixin The `BlocErrorHandler` mixin provides `_safeEmit()` wrapper: **Location**: `apps/mobile/packages/core/lib/src/presentation/mixins/bloc_error_handler.dart` ```dart void _safeEmit(void Function(S) emit, S state) { try { emit(state); } on StateError catch (e) { // Bloc was closed before emit - log and continue gracefully developer.log( 'Could not emit state: ${e.message}. Bloc may have been disposed.', name: runtimeType.toString(), ); } } ``` **Usage in Cubits/Blocs**: ```dart 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)); // ✅ If BLoC disposed before emit, _safeEmit catches StateError gracefully }, onError: (errorKey) { return state.copyWith(status: ProfileStatus.error); }, ); } ``` ### 9.3 Pattern Summary | Pattern | When to Use | Risk | |---------|------------|------| | Singleton + BlocProvider.value() | Long-lived features (Profile, Shifts, etc.) | Low - instance persists | | Transient + BlocProvider(create:) | Temporary widgets (Dialogs, Overlays) | Medium - requires careful disposal | | Direct BlocBuilder | Leaf widgets needing data | Low - no registration needed | **Remember**: - Use **singletons** for feature-level cubits accessed from multiple pages - Use **transient** only for temporary UI states - Always wrap emit() in `_safeEmit()` via `BlocErrorHandler` mixin - Test navigation away during async operations to verify graceful handling ```