diff --git a/docs/MOBILE/01-architecture-principles.md b/docs/MOBILE/01-architecture-principles.md index f151673a..b37833ca 100644 --- a/docs/MOBILE/01-architecture-principles.md +++ b/docs/MOBILE/01-architecture-principles.md @@ -219,3 +219,160 @@ See **`03-data-connect-connectors-pattern.md`** for comprehensive documentation - 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 + +```