Build expense-type-picker-widget layout grid
epic-expense-type-selection-user-interface-task-003 — Compose the four ExpenseTypeOptionCards into a responsive 2×2 grid layout inside the ExpenseTypePickerWidget. The widget must be stateless, accept a list of ExpenseTypeOption models and a disabled set from the BLoC stream, and emit an onSelectionChanged callback. No business logic inside the widget.
Acceptance Criteria
Technical Requirements
Execution Context
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.
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.
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.