mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

build: add flutter_animate

* plan animations

+279 -1
+241
docs/specs/animate.md
··· 1 + --- 2 + title: Micro-Animations Spec 3 + updated: 2026-04-27 4 + --- 5 + 6 + ## Summary 7 + 8 + Add polished micro-animations across the app using `flutter_animate` to bring 9 + life to transitions, state changes, and user interactions. The goal is subtle, 10 + fast, purposeful motion — not decorative. Every animation must serve 11 + orientation ("where am I?"), feedback ("did that work?"), or continuity 12 + ("what just changed?"). 13 + 14 + ## Package 15 + 16 + **`flutter_animate`** (pub.dev) — declarative, composable animation chains 17 + via extension methods on `Widget`. Chosen over raw `AnimationController` / 18 + `ImplicitlyAnimatedWidget` for consistency and velocity: a single API for 19 + fade, slide, scale, blur, shimmer, and custom effects, with built-in 20 + stagger support. 21 + 22 + Add to `pubspec.yaml`: 23 + 24 + ```yaml 25 + dependencies: 26 + flutter_animate: ^4.5.2 27 + ``` 28 + 29 + ## Animation Inventory 30 + 31 + ### 1. Feed & List Items — Staggered Entrance 32 + 33 + **Where:** Post cards in feed, notification rows, search results, follow 34 + audit results, list members, saved posts. 35 + 36 + **Effect:** Each item fades in + slides up as it enters the viewport for the 37 + first time (initial load or pagination append). 38 + 39 + ```dart 40 + child 41 + .animate() 42 + .fadeIn(duration: 200.ms, curve: Curves.easeOut) 43 + .slideY(begin: 0.05, end: 0, duration: 200.ms, curve: Curves.easeOut) 44 + ``` 45 + 46 + Stagger: 50ms offset per item (capped at 10 items per batch to avoid long 47 + entrance sequences on large pages). 48 + 49 + **Constraint:** Only on first appearance. Scrolling back to an already-seen 50 + item must not re-animate. Track via a `Set<String>` of post URIs (or item 51 + keys) in the feed cubit/bloc. 52 + 53 + ### 2. Action Feedback — Like, Repost, Bookmark 54 + 55 + **Where:** Post action bar icons. 56 + 57 + **Effect:** On tap, the icon scales up briefly then settles back, with a 58 + color crossfade to the active tint. 59 + 60 + ```dart 61 + icon 62 + .animate(onPlay: (c) => c.forward()) 63 + .scale(begin: 1.0, end: 1.3, duration: 120.ms, curve: Curves.easeOut) 64 + .then() 65 + .scale(begin: 1.3, end: 1.0, duration: 100.ms, curve: Curves.easeOutBack) 66 + ``` 67 + 68 + The color change uses `AnimatedSwitcher` or `ColorTween` on the existing 69 + icon — `flutter_animate`'s `.tint()` effect is an alternative. 70 + 71 + ### 3. Screen Transitions — Fade-Through 72 + 73 + **Where:** All `GoRouter` page transitions (currently using default Material 74 + platform transitions). 75 + 76 + **Effect:** Material fade-through (outgoing screen fades out + scales down 77 + slightly, incoming screen fades in + scales up slightly). Duration 300ms. 78 + 79 + Implement via a custom `TransitionPage` wrapper: 80 + 81 + ```dart 82 + class FadeThroughPage<T> extends CustomTransitionPage<T> { 83 + FadeThroughPage({required super.child, super.key}) 84 + : super( 85 + transitionsBuilder: (context, animation, secondaryAnimation, child) { 86 + return FadeThroughTransition( 87 + animation: animation, 88 + secondaryAnimation: secondaryAnimation, 89 + child: child, 90 + ); 91 + }, 92 + transitionDuration: const Duration(milliseconds: 300), 93 + ); 94 + } 95 + ``` 96 + 97 + Use `animations` package's `FadeThroughTransition` or implement manually 98 + with `flutter_animate` chained fade + scale. 99 + 100 + ### 4. Skeleton / Shimmer Loading 101 + 102 + **Where:** Feed loading placeholders, profile header loading, notification 103 + list loading — anywhere a shimmer placeholder is shown. 104 + 105 + **Effect:** Replace static grey boxes with a shimmer sweep using 106 + `.shimmer(duration: 1200.ms, color: theme.colorScheme.surfaceContainerHigh)`. 107 + 108 + ```dart 109 + Container( 110 + height: 16, width: 120, 111 + decoration: BoxDecoration( 112 + color: theme.colorScheme.surfaceContainerHighest, 113 + borderRadius: BorderRadius.zero, // square geometry per UI refactor 114 + ), 115 + ) 116 + .animate(onPlay: (c) => c.repeat()) 117 + .shimmer(duration: 1200.ms, color: theme.colorScheme.surfaceContainerHigh) 118 + ``` 119 + 120 + ### 5. Bottom Navigation Bar — Icon Transition 121 + 122 + **Where:** Bottom nav bar active/inactive icon swap. 123 + 124 + **Effect:** Active icon scales up from 1.0 to 1.15 with a fade crossfade 125 + between outlined → filled icon variants. Duration 150ms. 126 + 127 + ### 6. Snackbar / Toast Entrance 128 + 129 + **Where:** All snackbar and toast messages. 130 + 131 + **Effect:** Slide up from bottom + fade in (200ms). On dismiss, fade out + 132 + slide down (150ms). 133 + 134 + ### 7. FAB / Action Button 135 + 136 + **Where:** Compose FAB, gallery FAB, scroll-to-top button. 137 + 138 + **Effect:** Scale-in on appear (`scaleXY` 0 → 1, 200ms, `Curves.easeOutBack`). 139 + Scale-out on disappear (reverse). 140 + 141 + ### 8. Pull-to-Refresh Indicator 142 + 143 + **Where:** Feed and list pull-to-refresh. 144 + 145 + **Effect:** The refresh indicator rotates continuously during refresh, then 146 + scales down + fades out on completion (200ms). 147 + 148 + ### 9. Profile Header — Parallax 149 + 150 + **Where:** Profile screen banner image. 151 + 152 + **Effect:** Subtle parallax on scroll — banner moves at 0.5x scroll speed. 153 + Implemented via `SliverAppBar`'s existing `flexibleSpace` with a 154 + `Transform.translate` driven by scroll offset, not `flutter_animate`. 155 + 156 + ### 10. Empty State Illustrations 157 + 158 + **Where:** Empty feeds, no search results, no notifications. 159 + 160 + **Effect:** Fade in + gentle scale from 0.95 to 1.0 (300ms, ease-out). 161 + Prevents the "flash" of empty state content. 162 + 163 + ## Animation Tokens 164 + 165 + Centralise timing and curves in a single file to keep motion consistent: 166 + 167 + **`lib/core/theme/animation_tokens.dart`** 168 + 169 + ```dart 170 + abstract final class Anim { 171 + // Durations 172 + static const fast = Duration(milliseconds: 150); 173 + static const normal = Duration(milliseconds: 250); 174 + static const slow = Duration(milliseconds: 400); 175 + 176 + // Curves 177 + static const enter = Curves.easeOut; 178 + static const exit = Curves.easeIn; 179 + static const emphasis = Curves.easeOutBack; 180 + 181 + // Stagger 182 + static const staggerOffset = Duration(milliseconds: 50); 183 + static const maxStaggerItems = 10; 184 + } 185 + ``` 186 + 187 + All animations in the codebase reference these tokens — no magic numbers 188 + in widget files. 189 + 190 + ## Reduced Motion 191 + 192 + Respect the platform's "reduce motion" accessibility setting: 193 + 194 + ```dart 195 + final reduceMotion = MediaQuery.of(context).disableAnimations; 196 + ``` 197 + 198 + When `reduceMotion` is true, skip all non-essential animations. Essential 199 + transitions (screen changes) use a simple crossfade at 150ms. Loading 200 + shimmers continue (they convey information, not decoration). 201 + 202 + Wrap in a utility: 203 + 204 + ```dart 205 + extension AnimateAccessibility on Widget { 206 + Widget animateIfAllowed(BuildContext context, List<Effect> effects) { 207 + if (MediaQuery.of(context).disableAnimations) return this; 208 + return animate(effects: effects); 209 + } 210 + } 211 + ``` 212 + 213 + ## Performance 214 + 215 + - All animations use `flutter_animate`'s transform-based effects (GPU 216 + composited) — avoid layout-triggering properties like width/height 217 + animation on list items. 218 + - Stagger caps at 10 items to avoid jank on low-end devices. 219 + - Profile the animation layer with Flutter DevTools' performance overlay 220 + before merge — target 0 skipped frames on a mid-range Android device 221 + (Pixel 6a equivalent). 222 + - Feed item animations are fire-once; re-scrolling does not re-trigger. 223 + 224 + ## Testing 225 + 226 + - Unit test `Anim` token values (sanity check, constants don't drift). 227 + - Widget tests for animated widgets use `tester.pumpAndSettle()` to 228 + complete animations, then assert final visual state. 229 + - Widget tests for reduced-motion: set `MediaQuery.disableAnimations` 230 + to `true`, verify no `Animate` widget in the tree. 231 + - Golden tests for shimmer placeholders (capture mid-animation frame). 232 + 233 + ## Scope & Constraints 234 + 235 + - **No animated illustrations or Lottie.** All motion is CSS-style 236 + property animation — no custom vector art or frame-based animation. 237 + - **No animation preferences screen.** We respect the OS-level reduced 238 + motion toggle only. A per-app toggle is out of scope. 239 + - **No physics-based animations** (springs, flings) in this pass. 240 + `flutter_animate` supports them but they're harder to keep consistent. 241 + Evaluate in a future milestone.
+21 -1
docs/tasks/phase-8.md
··· 1 1 --- 2 2 title: Phase 8 Task Breakdown 3 - updated: 2026-04-11 3 + updated: 2026-04-27 4 4 --- 5 5 6 6 ## M27 - Follow Hygiene: Detect & Remove Inactive/Problematic Follows 7 7 8 8 Completed [2026-04-11](../../CHANGELOG.md#2026-04-11) 9 + 10 + ## M28 - Micro-Animations with flutter_animate 11 + 12 + Spec: [animate.md](../specs/animate.md) 13 + 14 + - [x] Add `flutter_animate` dependency 15 + - [ ] Create `lib/core/theme/animation_tokens.dart` with centralised durations, curves, stagger constants 16 + - [ ] Add reduced-motion utility (`animateIfAllowed` extension) 17 + - [ ] Add setting for users to turn off animations 18 + - [ ] Feed & list items: staggered fade-in + slide-up on first appearance (track seen items to avoid re-animation) 19 + - [ ] Action feedback: scale-bounce on like / repost / bookmark tap 20 + - [ ] Screen transitions: fade-through `TransitionPage` wrapper for GoRouter 21 + - [ ] Shimmer loading: replace static skeleton placeholders with `.shimmer()` sweep 22 + - [ ] Bottom nav bar: scale + crossfade on active icon change 23 + - [ ] Snackbar / toast: slide-up entrance, fade-out dismiss 24 + - [ ] FAB / action buttons: scale-in on appear, scale-out on disappear 25 + - [ ] Pull-to-refresh: rotate + fade-out on completion 26 + - [ ] Profile header: parallax banner on scroll 27 + - [ ] Empty state: gentle fade + scale entrance 28 + - [ ] Widget tests for all animated widgets (pumpAndSettle, reduced-motion branch)
+16
pubspec.lock
··· 494 494 description: flutter 495 495 source: sdk 496 496 version: "0.0.0" 497 + flutter_animate: 498 + dependency: "direct main" 499 + description: 500 + name: flutter_animate 501 + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" 502 + url: "https://pub.dev" 503 + source: hosted 504 + version: "4.5.2" 497 505 flutter_bloc: 498 506 dependency: "direct main" 499 507 description: ··· 526 534 url: "https://pub.dev" 527 535 source: hosted 528 536 version: "2.0.34" 537 + flutter_shaders: 538 + dependency: transitive 539 + description: 540 + name: flutter_shaders 541 + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" 542 + url: "https://pub.dev" 543 + source: hosted 544 + version: "0.1.3" 529 545 flutter_svg: 530 546 dependency: "direct main" 531 547 description:
+1
pubspec.yaml
··· 50 50 objectbox: ^5.3.1 51 51 objectbox_flutter_libs: ^5.3.1 52 52 tflite_flutter: ^0.12.1 53 + flutter_animate: ^4.5.2 53 54 54 55 dev_dependencies: 55 56 flutter_test: