critical priority medium complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

ExpenseCalculationPreview subscribes to ExpenseSelectionBLoC via BlocBuilder (preferred over raw StreamBuilder for consistency with BLoC pattern used in the project)
Widget re-renders whenever the calculationResult field on ExpenseSelectionState changes
No setState or local state used — all state derives from BLoC stream
Integration test confirms preview panel reflects correct CalculationResult within 300ms of a selection change event being dispatched to the BLoC
While calculation is in progress (if BLoC emits a loading state), the panel shows a loading indicator or retains the last result — no blank flash
BLoC subscription is properly disposed — no stream leak when widget is removed from the tree
buildWhen predicate is used to limit rebuilds to only when calculationResult changes, not on every BLoC state emission

Technical Requirements

frameworks
Flutter
BLoC
flutter_test
data models
ExpenseSelectionState (contains calculationResult: CalculationResult?)
CalculationResult
ExpenseSelectionBLoC
performance requirements
Preview update latency ≤ 300ms from event dispatch to widget rebuild — measured in integration test
BlocBuilder buildWhen must filter out irrelevant state changes to prevent unnecessary rebuilds
ui components
BlocBuilder<ExpenseSelectionBLoC, ExpenseSelectionState> with buildWhen
ExpenseCalculationPreview (stateless, data-driven)

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use BlocBuilder rather than StreamBuilder directly to remain consistent with BLoC conventions already established in the codebase. Add buildWhen: (previous, current) => previous.calculationResult != current.calculationResult to the BlocBuilder to prevent rebuilds when other fields change (e.g., loading state for a different operation). The 300ms SLA is a product requirement from HLF's detail requirements — enforce it in the integration test using fake async timing, not real timers. If calculation is async (Supabase query or local computation), ensure the BLoC emits an intermediate state with the previous result preserved, not null, to avoid a blank flash on every keypress.

Dispose the BLoC at the parent screen level, not inside this widget.

Testing Requirements

Integration tests using flutter_test with a fake/mock ExpenseSelectionBLoC. Cover: (1) initial render shows placeholder when BLoC state has null calculationResult, (2) dispatching a selection event causes preview to update within 300ms — use tester.pump(Duration(milliseconds: 300)) and assert updated state, (3) multiple rapid selection changes result in final correct state (no stale intermediate state displayed), (4) widget disposes BLoC subscription without errors when popped from navigation stack, (5) buildWhen verified by counting build calls — emitting an unrelated state field change must not trigger a rebuild.

Component
Expense Calculation Preview
ui low
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.