Visual timers — requirements
Epic. MVP-1 requirements breakdown for the Visual timers differentiator (see the feature architecture). The timer is a pure presentation atom backed by a local ticker; it introduces no new domain — it consumes the existing
estimateMinfield on the chore model. This is the primary time-blindness affordance and a declared neurodiversity differentiator.
| Type | Functional ⭐ |
| Layer (build approach) | Frontend — design_system atom + local ticker |
| RICE | R 6 × I 2 × C 80% / E 2 = 4.8 · Tier MVP-1 |
| KPI (summary) | Timed-chore on-time completion lift — see Success criteria |
| Traces to | feature visual-timers · C4 design-system |
| Depends on | Today (estimateMin) · Analytics |
Success criteria (definitive KPI)
Success = for timed chores, on-time completion lifts by ≥ 10 percentage points vs
untimed chores in the same household, OR ≥ 65% of timed chores record a
timer_started event — measured across weeks 2–4 post-activation.
- Metric A (adoption): share of chores with
estimateMin > 0that emittimer_startedper week. Target: ≥ 65% by week 4. - Metric B (lift): on-time rate for timed chores vs the same household's untimed
baseline. On-time =
chore_completedarriving withinestimateMinoftimer_started. Target: ≥ +10 pp. - Window: weeks 2–4 post-activation (same cohort anchor as the Today KPI).
- Guardrail:
timer_cancelled÷timer_started< 30% — excess cancellation signals the timer is disruptive, not supportive. - Baseline: no install base yet; treat these numbers as the launch hypothesis, then re-baseline from first-cohort wk-2 data before declaring success or failure.
Analytics — events to record
All events are internal-BI only: content-free, no child identity, no chore content, never marketing. Emitted through the consent-gated analytics substrate. COPPA: opaque session ref only — never a member name or age-linked identifier (COPPA).
| Event | When | Key properties | Feeds |
|---|---|---|---|
timer_started | Member taps start on a timed chore | duration_bucket (≤5m|≤15m|≤30m|>30m), reduced_motion (bool) | adoption numerator (Metric A) |
timer_completed | Ticker reaches zero naturally | duration_bucket, overrun_bucket (on_time|<30s|<2m|>2m) | on-time lift (Metric B); completion funnel |
timer_cancelled | Member dismisses before expiry | duration_bucket, pct_elapsed_bucket (<25|<50|<75|<100) | guardrail; UX signal |
These three events are the minimum instrumentation required for the KPI — both
timer_started(adoption denominator) andtimer_completed(lift numerator) ship with the surface, not after.
Scope
The visual timer is a design_system atom (DsTimerRing or equivalent) that renders
elapsed/remaining time as a shrinking arc or fill — concrete, spatial, not abstract
digits. It is wired into the Today surface for any chore where estimateMin > 0. State
is local: the ticker lives in a lightweight presentation controller with no Bloc event per
tick and no server round-trip. It pauses when the app is backgrounded. Reduced-motion and
a11y variants are required from day one.
Functional requirements
FR-TIMER-1 — DS timer atom: animated arc/fill
Priority: P1 · Status: ❌ not built Statement. As a child member with time-blindness, I see a big, concrete visual of time passing (shrinking arc or fill) so I understand how much time remains without reading a clock number. Acceptance
- Given
estimateMin > 0and a running ticker When the atom renders Then a filled arc or ring shrinks proportionally from 100% → 0% as elapsed time increases. - Given
elapsed >= totalWhen expiry fires Then the atom reaches its empty/expired state and callsonExpired; it does not auto-change the chore status. - Atom is model-agnostic: accepts
(Duration total, Duration elapsed)— zeroclient_sdkimport. - Animation uses compositor-only properties (arc sweep, opacity); no layout-bound properties animate per frame.
FR-TIMER-2 — Reduced-motion + a11y variant
Priority: P1 · Status: ❌ not built Statement. As a member with reduced-motion enabled or who uses a screen reader, the timer remains fully usable without relying on animation or colour alone. Acceptance
- Given
MediaQuery.of(context).disableAnimations == trueWhen the atom renders Then the continuous sweep is replaced by a discrete percentage label updating at ≤ 10-second intervals. - Arc stroke/fill achieves ≥ 3:1 contrast against its background at all fill levels (WCAG 1.4.3 non-text).
- A
Semanticslabel announces remaining time (e.g."8 minutes remaining") and updates at ≤ 30-second intervals — not every tick — to avoid screen-reader noise. - Expiry is announced once via
SemanticsService.announce.
FR-TIMER-3 — Local ticker controller
Priority: P1 · Status: ❌ not built Statement. As a developer, the ticker is a self-contained presentation controller so that timer precision does not inflate Bloc event volume or trigger unnecessary widget rebuilds. Acceptance
- Given the timer is running When each frame fires Then only the
DsTimerRingsubtree rebuilds (viaValueListenableBuilderover aValueNotifier<Duration>); theTodayChoresBlocemits zero events per tick. - Controller exposes
start(),pause(),resume(),cancel()with immutable state transitions. - Elapsed time is derived from wall-clock delta (not accumulated increments) so background jitter does not drift the displayed remaining time.
FR-TIMER-4 — Lifecycle: pause when backgrounded
Priority: P1 · Status: ❌ not built Statement. As a member returning from a background task, I see an accurate remaining time — not a drifted or already-expired timer. Acceptance
- Given the app transitions to
AppLifecycleState.pausedWhen the ticker is active Then it pauses immediately and records a wall-clock snapshot. - Given the app resumes When the ticker was paused by lifecycle Then elapsed time is recomputed from the wall-clock snapshot and ticking resumes without a visible gap.
- No
Timer.periodicfires while backgrounded; battery impact from the timer controller is zero during background.
FR-TIMER-5 — Today surface integration
Priority: P1 · Status: ❌ not built Statement. As a member on the Today surface, I can start a visual timer for any timed chore without navigating away from the chore row. Acceptance
- Given a chore row with
estimateMin > 0When Today renders Then a start-timer affordance (icon or pill) is visible alongside the chore. - Given I tap start When the ticker is running Then
DsTimerRingrenders inline; surrounding chore rows do not reflow. - Given the timer expires When
onExpiredfires Then the row returns to its normal state; an optional local haptic fires (platform-permitted, user-controllable); chore status is not auto-changed. - The timer affordance is absent for chores without
estimateMin.
FR-TIMER-6 — Analytics instrumentation
Priority: P1 · Status: ❌ not built Statement. As the product team, I can measure timer adoption and on-time lift using the three events in the Analytics table — with zero child PII in any payload. Acceptance
- Given any timer action When the event is emitted Then the payload matches the
schema above: correct
duration_bucket,reduced_motion,overrun_bucket, orpct_elapsed_bucketas applicable. - No chore title, member name, household ID, or member-linked identifier appears in any timer event.
- Events are suppressed when analytics consent is absent (consent-gated substrate).
Architecture considerations
- Pure presentation, no new domain.
DsTimerRinglives indesign_systemwith noclient_sdkdependency.estimateMinis already on the chore model — no schema change required. - One data path unchanged.
TodayChoresBlocsurfacesestimateMinfrom the repository; the ticker controller is instantiated in the widget tree, not inside the Bloc. - Compositor-friendly animation. Drive with a Flutter
AnimationControllerorCustomPainterarc sweep; no layout invalidation per frame. - Reduced-motion. Branch on
MediaQuery.of(context).disableAnimationsat render time; single atom, single code path, no separate widget class. - Battery / lifecycle.
WidgetsBindingObserverin the controller; useTicker(frame-budget-aware) notTimer.periodic; pauseTickeron background. - Rebuild scope.
ValueNotifier<Duration>+ValueListenableBuilder— zero Bloc events per tick, minimal rebuild surface.
Design work (ahead of build)
- Timer atom visual (core design artifact): arc style (ring vs fill vs segmented),
colour semantics (green → amber → red as time runs low with defined thresholds),
expiry state (empty ring + icon or brief pulse). Both light and dark themes; inherits
DsThemetokens — no hardcoded colours. - Reduced-motion variant: discrete step layout — percentage label + segmented progress indicator replacing the continuous sweep.
- On-Today placement: inline affordance in the chore row (start icon/pill) and the active timer state (full ring or compact arc); must not reflow adjacent rows.
Decisions (resolved for MVP-1)
Resolved — see the MVP-1 decisions log for the canonical record, rationale, and status legend (✅ decided · ⚖️ counsel confirms · 🔜 MVP-1.x).
- D-TIMER-1 — Expiry signal + admin disable (FR-TIMER-5). ✅ Expiry fires a gentle haptic + optional local notification; the household admin can disable the notification (haptic stays unless the device is muted).
- D-TIMER-2 — Timer in approval view. ✅ Yes — show elapsed/estimate on the approval card so a reviewer sees how long the chore took. Reuses the atom.
- D-TIMER-3 — Estimate adjustment. ✅ Admin-set estimate is the default; a member may adjust their own run's estimate within bounds — not the chore's canonical estimate.
- D-TIMER-4 — Colour thresholds. ✅ Green ≥ 50%, amber < 50%, red < 25% remaining; encode as colour tokens. Under reduced-motion the colour still shifts but does not pulse.
Out of scope (MVP-1)
- Per-step timers inside a Routine sequence → post-MVP (Routines feature).
- Auto-overdue status change on timer expiry → open decision; not built for MVP-1.
- Timer state sync to the server or other devices → purely local for MVP-1.
- Admin approval view showing elapsed time → post-MVP.
- Customisable colour themes for the atom beyond
DsThemetokens → deferred.