high priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

When reduced-motion is false the value display animates from 0 to the target integer over the controller-defined duration using an ease-out curve
When reduced-motion is true the value display shows the final value immediately on first render — no intermediate frames with intermediate numbers
The displayed intermediate values are formatted with the same locale-aware number formatting used in the final value (thousand separators, no decimals for integer targets)
The animation respects the duration exposed by WrappedAnimationController; changing the controller's duration changes the animation duration without modifying StatCardWidget
If the target value changes while animation is in progress, the animation restarts from the current animated value toward the new target (not from zero)
The animation completes exactly at the target value — no overshoot, no lingering on an off-by-one intermediate value
Disposing the widget during animation does not throw exceptions or leak animation resources
Widget tests confirm: (1) final value rendered when reduced-motion is true; (2) animated intermediate value rendered mid-animation when reduced-motion is false; (3) no exception on dispose during animation

Technical Requirements

frameworks
Flutter
flutter_test
apis
AnimationController
Tween<double>
CurvedAnimation
WrappedAnimationController (project internal)
SummaryAccessibilityProvider
performance requirements
Animation must run at 60 fps on low-end devices without dropped frames — use AnimatedBuilder, not setState in addListener
No allocations inside the AnimatedBuilder builder callback — pre-compute the NumberFormat instance outside the build loop
ui components
StatCardWidget (value display zone)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement count-up inside StatCardWidget using an AnimationController obtained from (or delegated by) WrappedAnimationController. Use a Tween(begin: 0, end: targetValue.toDouble()) with a CurvedAnimation(parent: controller, curve: Curves.easeOut). In the AnimatedBuilder builder, format the current animation.value.round() with the pre-instantiated NumberFormat. When SummaryAccessibilityProvider.isReducedMotion is true, do not create an AnimationController at all — render the final value directly.

This avoids creating and immediately disposing an unnecessary controller. Use a late final field pattern to lazily initialise the controller only when reduced-motion is false. If the target value changes (widget rebuild with new value), call controller.animateTo(newTarget) rather than re-initialising the controller, to allow smooth mid-flight transitions.

Testing Requirements

Write widget tests using FakeAsync to control animation time: (1) pump to 50% of duration and verify the displayed integer is between 0 and the target; (2) pump to completion and verify the exact target is displayed; (3) set reduced-motion to true and verify target is displayed at frame 0 with no animation ticks. Write a dispose test that mounts, starts animation, then unmounts mid-animation and verifies no exception is thrown. Use flutter_test's tester.pumpAndSettle for completion assertions.

Component
Animated Stat Card Widget
ui medium
Epic Risks (2)
medium impact medium prob technical

Simultaneous count-up animations across multiple stat cards and chart draw-in animations on lower-end Android devices may cause frame drops below 60fps, degrading the premium Wrapped experience and making the feature feel unpolished.

Mitigation & Contingency

Mitigation: Stagger animation starts using AnimationController with staggered intervals rather than starting all animations simultaneously. Use RepaintBoundary around each animated widget to isolate rasterisation. Profile on a mid-range Android device (e.g., equivalent to Pixel 4a) during development, not just at QA.

Contingency: If frame rate targets cannot be met on low-end devices, implement a device-capability check at startup and substitute simpler fade-in animations for the count-up and chart draw-in on devices below a CPU performance threshold.

medium impact low prob integration

The activity-type-breakdown-widget must render organisation-specific activity type labels sourced from the terminology system. If the terminology provider is not yet integrated at the time this widget is built, the widget will display hardcoded system labels, which is a regression risk for multi-org support.

Mitigation & Contingency

Mitigation: Accept activity type labels as a typed parameter in the widget constructor rather than reading from the terminology provider directly inside the widget. The BLoC or repository layer resolves labels before passing them to the widget, maintaining clean separation and testability.

Contingency: If terminology resolution is unavailable at widget integration time, display internal activity type keys as a temporary fallback with a localised suffix '(label pending)' visible only in non-production builds so QA can identify unresolved labels.