critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

A BlocBuilder (or BlocConsumer if side effects required) wraps ExpenseTypePickerWidget and feeds it BLoC state
ExpenseSelectionBLoC emits an ExpenseSelectionState containing: List<ExpenseTypeOption> availableTypes, Set<String> selectedTypeIds, Set<String> disabledTypeIds
On card tap, the screen dispatches ExpenseTypeSelected(typeId: id) event to the BLoC
BLoC state changes cause the widget tree to rebuild with updated selected/disabled sets — no stale UI
If BLoC emits a loading state, the widget shows a loading indicator (skeleton or CircularProgressIndicator) instead of the grid
If BLoC emits an error state, the widget shows an accessible error message with a retry option
The integration screen passes all BLoC state down via widget props only — no direct BLoC.of() calls inside ExpenseTypePickerWidget or ExpenseTypeOptionCard
Widget tests using bloc_test and mocktail confirm correct event dispatch and state mapping

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
flutter_test
bloc_test
mocktail
data models
ExpenseSelectionState
ExpenseTypeSelected
ExpenseTypeOption
performance requirements
BlocBuilder buildWhen should limit rebuilds to only when selectedTypeIds or disabledTypeIds change
Avoid rebuilding the entire picker on unrelated BLoC state changes
security requirements
BLoC state must not expose raw database row IDs to the UI layer — use domain model IDs only
ui components
BlocBuilder<ExpenseSelectionBloc, ExpenseSelectionState>
ExpenseTypePickerWidget (from task-003)
LoadingIndicator widget
ErrorRetryWidget

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use flutter_bloc's BlocBuilder rather than raw StreamBuilder for consistency with the rest of the codebase. Define buildWhen: (prev, curr) => prev.selectedTypeIds != curr.selectedTypeIds || prev.disabledTypeIds != curr.disabledTypeIds to prevent unnecessary rebuilds on unrelated state changes. The BLoC itself (mutual exclusion logic, API calls) is out of scope for this task — assume it exists and expose its API as a contract. Define the ExpenseSelectionEvent and ExpenseSelectionState sealed classes before starting integration.

Handle all three BLoC state variants (loading, loaded, error) to avoid blank screens. Keep the BlocBuilder in a parent screen widget (e.g., ExpenseTypeSelectionScreen) and pass pure props into ExpenseTypePickerWidget to preserve its testability.

Testing Requirements

Widget + BLoC integration tests: (1) mock ExpenseSelectionBloc with mocktail, emit a loaded state, and verify the picker renders with correct selectedIds and disabledIds, (2) simulate a card tap and verify ExpenseTypeSelected event is added to the bloc, (3) emit a loading state and verify the loading indicator appears, (4) emit an error state and verify the error widget appears with retry button, (5) emit two successive states and verify the widget rebuilds exactly once per emission with buildWhen applied. Use bloc_test helpers (blocTest, whenListen) for clean test setup.

Component
Expense Type Picker Widget
ui medium
Epic Risks (2)
medium impact medium prob technical

If the expense calculation preview subscribes to the full BLoC state stream, every unrelated state property change (e.g. a loading flag toggle) triggers a widget rebuild. With complex card animations for the disabled-state transition, this could cause frame drops on low-end Android devices used by some peer mentors.

Mitigation & Contingency

Mitigation: Use select() on the Riverpod provider to subscribe only to the specific state slice each widget needs; write a performance test asserting rebuild count on a rapid sequence of toggle events.

Contingency: If jank is detected in device testing, replace animated disabled-state transitions with instant opacity changes and defer animation polish to a follow-up sprint.

medium impact low prob integration

The disabled card state requires a specific contrast-safe colour combination that communicates unavailability without relying solely on colour (WCAG 1.4.1). If the current design token palette does not include a disabled-state token with sufficient contrast for text on the disabled background, the widget will either fail WCAG AA or require a last-minute design token addition that could break other components.

Mitigation & Contingency

Mitigation: Audit the existing design token manifest for disabled-state tokens at the start of the epic; if missing, raise with the design lead and add a contrast-validated token before widget implementation begins.

Contingency: If no design review is available, use the established --color-text-disabled and --color-surface-disabled tokens with an added strikethrough or lock icon to satisfy WCAG 1.4.1 non-colour requirement, and document the deviation for design review.