critical priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

ExpenseTypePickerWidget is a StatelessWidget accepting: List<ExpenseTypeOption> options, Set<String> selectedIds, Set<String> disabledIds, void Function(String selectedId) onSelectionChanged
Layout renders a 2×2 grid using GridView or a Wrap/Column+Row composition with equal-sized cells
Grid adapts responsively: on narrow screens (< 360dp) cards stack in a single column, on standard screens they render 2-per-row
Each card receives isSelected = selectedIds.contains(option.id) and isDisabled = disabledIds.contains(option.id) — no logic beyond this mapping
Tapping an enabled card fires onSelectionChanged(option.id); disabled cards do not fire the callback
Widget contains zero business logic — no if-conditions based on expense type names, no mutual exclusion rules
Widget is fully testable in isolation without a BLoC dependency
Golden tests pass for the 2×2 layout at 360dp and 414dp screen widths

Technical Requirements

frameworks
Flutter
flutter_test
data models
ExpenseTypeOption
performance requirements
Grid renders in under 16ms for 4 items on mid-range hardware
Use const constructors wherever possible to prevent unnecessary rebuilds
ui components
ExpenseTypePickerWidget (new)
ExpenseTypeOptionCard (from task-001/002)
GridView or Row/Wrap layout primitive

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Prefer a fixed 2-column GridView with childAspectRatio tuned to the design spec over a manual Row layout — GridView handles edge cases like odd item counts more reliably. Use a crossAxisCount of 2 and calculate childAspectRatio from the design token card height and half the screen width minus padding. The widget must be truly stateless: it maps input props to child widget props and forwards callbacks. Adding any local state (e.g., a local selectedId variable) is a violation of the architecture contract.

Consider extracting a _buildOptionCard() private method to keep build() readable. For the HLF use case, the mutual exclusion rule (km + bus ticket cannot be combined) will be enforced by the BLoC layer; the widget simply renders whatever disabledIds it receives.

Testing Requirements

Widget tests: (1) render 4 cards with mixed selected/disabled states and verify correct props are passed to each card, (2) tap an enabled card and verify onSelectionChanged fires with the correct id, (3) tap a disabled card and verify onSelectionChanged does not fire, (4) render with 1 and 3 options to verify the grid handles non-square counts gracefully, (5) golden tests at two breakpoints (360dp width and 414dp width). Test coverage: 100% of public API paths.

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.