agentleFS
ariaraminariaramin/flutter-skills/skills/flutter-motion-engineering/SKILL.md

flutter-motion-engineering

Skill8 starsChanged 48 days agoMITView on GitHub

Design, implement, review, debug, and profile Flutter animations and transitions with Apple-quality continuity, responsive gestures, and platform-appropriate behavior. Use for spring motion, drag-to-dismiss, interactive sheets, interrupted or reversing animations, Hero and route transitions, scroll-linked effects, reduced motion, animation lifecycle bugs, and motion-related jank, including requests to make Flutter interactions feel smooth or native. Do not use for SwiftUI/UIKit implementation, animation asset authoring, or backend performance. For Material animations-package APIs, use the package-specific skill when available.

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 6 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Read the core workflow first, then load only the supporting material required for the task.

    line 19
  2. Distinctive:

    Keep interpolation local to presentation.

    line 35
  3. Distinctive:

    Test behavior and lifecycle.

    line 36
  4. Very specific:

    D/E: Keep lazy slivers/list builders and stable entity keys.

    line 130
  5. Very specific:

    E, arithmetic: Use the observed frame interval: 1000 / refreshRateHz milliseconds.

    line 140
  6. Very specific:

    Use Inspector for tree/constraint/repaint investigation, not FPS certification.

    line 308
  7. Very specific:

    Test retarget/reverse from a nonterminal position.

    line 316
  8. Distinctive:

    Test zero/changed extent, RTL, large text, nested navigation/scrolling, back-gesture cancellation, and rapid child replacement where …

    line 318
  9. Distinctive:

    Use goldens for a few deterministic visual states with controlled fonts/images.

    line 319
  10. Distinctive:

    Use an integration/performance scenario on representative hardware for pacing.

    line 320

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Flutter Motion Engineering

Purpose and evidence policy

Engineer motion that communicates state, follows input, survives interruption, and delivers consistent frames. Treat Apple quality as continuity, restraint, and responsiveness, not an instruction to copy iOS styling onto every platform. Accessibility and correctness are acceptance gates, never optional performance tradeoffs.

Evidence labels used below: D = documented framework behavior; P = platform/version-specific guidance; E = engineering recommendation or inference. Selection tables and validation procedures are E unless stated otherwise. Distinguish confirmed defects, measured bottlenecks, plausible risks, and optional refinements in reviews.

Check flutter --version --machine, SDK constraints, lockfiles, and the actual renderer before applying version-sensitive advice. Official documentation is rolling: some pages still identify Flutter 3.44.7 while individual sections describe 3.47; API footers can show 0.0.0. Use the installed SDK source or a matching release tag to resolve conflicts. Do not infer a minimum supported version from a rolling API page.

Bundled reference routing

Read the core workflow first, then load only the supporting material required for the task.

TaskReference
Gesture state, velocity, retargeting, geometry changes, route ownershipInteraction protocols
Baseline capture, stage diagnosis, rendering experiments, evidence reportingProfiling playbook
Runnable examples, regression coverage, integration gates, validation limitsTesting and examples

The examples are a small Flutter test package, not an application template. Reuse the host project's architecture and tokens instead of copying an entire new motion layer.

Implementation workflow

  1. Inspect repository rules, affected widgets and every caller, route ownership, state management, tokens, accessibility, heavy content, and existing tests. Preserve unrelated changes.
  2. State the interaction: what changes, what relationship motion explains, who owns the target state, and whether input can interrupt it. Classify as decorative, state-driven, navigational, gesture-driven, or physics-driven.
  3. Define forward, reverse, cancellation, rapid retargeting, reduced motion, focus, hit testing, and target-platform behavior before choosing APIs.
  4. Reuse existing primitives and platform widgets. Choose the smallest mechanism that satisfies those behaviors; no animation is valid when motion adds no meaning.
  5. Keep interpolation local to presentation. Implement without per-frame business events or expensive synchronous work.
  6. Test behavior and lifecycle; profile the representative interaction; identify the expensive stage; change one relevant mechanism and re-profile.
  7. Report changed behavior, checks actually run, measured results, and remaining device/platform coverage. Never label an animation optimized from source inspection alone.

Motion decision framework and API selection

D: Animation<T> exposes changing values/status; AnimationController owns time and control; Tween<T> maps values; Curve remaps progress. Neither a tween nor a curve schedules frames. Flutter animation overview.

RequirementStart withBoundary / alternative
Simple target changesAnimatedContainer, AnimatedAlign, AnimatedOpacityInternal controllers are sufficient; choose explicit control for scrubbing or coordinated interruption.
Custom property with target interpolationTweenAnimationBuilderIt owns and mutates its tween; never share or mutate that tween elsewhere.
Two content states and size interpolationAnimatedCrossFadeCheck clipping, text scale, focus, and retained child cost.
Content replacementAnimatedSwitcher with semantic ValueKeyRapid replacement can retain several outgoing children; avoid UniqueKey() per build.
Content changes its natural sizeAnimatedSizeAccept layout work when surrounding geometry must change.
Position changes inside a StackAnimatedPositioned / directional equivalentIf footprint is unchanged, consider SlideTransition; position/size layout and paint translation are not equivalent.
Controlled fade, translation, scale, rotationFadeTransition, SlideTransition, ScaleTransition, RotationTransitionPrefer dedicated transition widgets over a large custom builder.
Controlled layout revealSizeTransitionIt changes layout and clips; do not classify it as a transform-only effect.
Several values on one timelineController + TweenSequence, Interval, CurvedAnimationUse one clock for truly coordinated motion; independent interactions need independent ownership.
Custom animated subtreeAnimatedBuilder + stable childUse ListenableBuilder for non-animation listenables; AnimatedWidget for a useful reusable boundary.
Drawing-only changesCustomPainter(repaint: listenable)Bypasses build/layout for ticks, not paint or GPU cost; provide semantics separately when needed.
Same object across routesExisting route + HeroNot a replacement for route state or arbitrary matched geometry.
Standard navigation / modalExisting adaptive route or Cupertino componentPreserve router integration; custom PageRouteBuilder only for a justified behavior gap.
Finger-following movement / momentumLocal controller + gesture updates + SimulationReuse existing sheet, dismissible, scrolling, or InteractiveViewer behavior first.

Selection mechanics: TweenAnimationBuilder, AnimatedSwitcher, AnimatedPositioned, CustomPainter.

Use implicit animation for changing targets without explicit timeline ownership. Retargeting from the current visual value does not establish physical velocity continuity. Use explicit control when cancellation, gesture progress, momentum, sequencing, or lifecycle behavior requires it. A CurvedAnimation can overshoot outside 0–1; constrain outputs such as opacity, and dispose owned curved animations/listeners. CurvedAnimation API.

Apple-quality motion: translate principles, not APIs

P/E: Keep visual cause and effect clear: an object should emerge from its source, return coherently, and follow the user's changing intent. Provide immediate press feedback, cancel it when the gesture loses, and leave essential actions available during transitions. Prefer existing button behavior, including keyboard, focus, semantics, and platform feedback. Do not make network completion wait for decorative settling. Apple HIG: Motion.

P/E: Preserve spatial hierarchy and continuity of position. Preserve velocity where an object is already moving or released from a gesture. A spring may be critically damped and nonbouncy; overshoot is not a quality requirement. Avoid lengthy travel, gratuitous parallax, and repeated entrance choreography on frequently used screens. Apple's spring presentation distinguishes perceived duration from physical settling; do not invent a universal Apple duration or damping preset. Apple: Animate with springs.

UIKit's paused scrubbing/reversal and SwiftUI's spring retargeting are interaction precedents, not Flutter API contracts. Flutter custom simulations require explicit handoff; animateTo() does not automatically reproduce SwiftUI's velocity-preserving spring replacement. UIViewPropertyAnimator, SwiftUI spring.

Curves, springs, and gesture-driven motion

NeedMechanismAvoid / verify
Known timing, small state changeDuration + curveRestarting an ease curve during a drag can break velocity continuity.
Natural settling or momentum handoffSpringSimulation + animateWithNo exact duration guarantee; verify settling tolerance and overshoot.
Object follows pointerDirect progress updatesDo not ease every pointer update or trail the finger through queued animations.
Drag followed by settlingDirect manipulation, then springConvert release velocity into simulation units and preserve its sign.
Scrolling / ballistic decelerationPlatform ScrollPhysicsDo not replace scroll machinery with a generic animation controller.

D/E: For mass m, stiffness k, damping c, damping ratio is c / (2 * sqrt(m * k)): below 1 is underdamped, 1 critically damped, above 1 overdamped. Tune response and settling together, starting with positive mass/stiffness and positive damping for settling interactions. Initial velocity uses position units per second, not normalized progress unless position itself is normalized. SpringDescription, SpringSimulation.

Prefer an existing project spring token. Where supported, SpringDescription.withDurationAndBounce offers duration/bounce parameterization documented as equivalent to SwiftUI's spring parameters; this does not guarantee identical gesture handling or rendering. Check the installed SDK before adopting it; use mass/stiffness/damping or withDampingRatio otherwise. Do not choose an undamped spring for an interaction expected to finish. Duration and bounce constructor.

Gesture-driven and interruptible motion

  1. Define one coordinate system: axis, origin, travel extent, bounds, and semantic endpoints. For normalized progress p = signedDisplacement / travel, release velocity is signedPixelsPerSecond / travel; reject nonfinite or nonpositive travel. Never substitute unsigned speed for signed axis velocity.
  2. On gesture acquisition, stop the settling simulation without resetting position. Account for gesture-arena competition and cancellation; a pointer-down event is not proof that a drag has won.
  3. While dragging, derive visual progress from input. Apply intentional bounds or resistance; do not replay a timed ease on each update. Preserve the pinch focal point when scaling and use InteractiveViewer when it meets the requirement.
  4. On release, select the target from direction, velocity, displacement, and valid semantic states. Use project thresholds, not unexplained device-independent magic numbers. On cancellation, settle to a valid state without accidentally committing dismissal.
  5. On programmatic retarget, capture the current position and controller.velocity before stopping, resetting, or assigning value; velocity becomes zero when stopped. Start the new spring from those captured values. Controller velocity.
  6. Handle re-grabbing, orientation/extent changes, nested scrolling, disposal, and late completion callbacks. Do not block all touches until settling ends. Commit business/navigation state once; animation completion is not the business transaction.

D: animateWith clamps values on bounded controllers and reports forward status even if the numeric target is lower. Use unbounded displacement when physical overshoot is intended, and track semantic direction separately. Clamp only constrained derived properties such as opacity; don't silently flatten the spring trajectory. animateWith API.

Navigation, shared elements, and platform behavior

SituationPreferred implementationRequired checks
Ordinary push/popExisting router's adaptive route; CupertinoPageRoute for deliberate iOS presentationBack gesture cancellation, route result, focus, RTL, restoration.
Modal / sheetExisting platform-appropriate modal APIBarrier label/dismissibility, nested navigator, keyboard/insets, drag/scroll handoff.
Card-to-detail continuityHero within existing route ownershipUnique matching tags, first-frame destination, compatible geometry and images.
Bespoke product transitionExisting router's custom page transition, or PageRouteBuilderPrimary and secondary animations, reverse, browser history, deep links, back gestures.

D/P: Cupertino routes implement iOS transitions, including different fullscreen-dialog behavior. A plain PageRouteBuilder slide does not recreate interactive Cupertino back navigation. Preserve Android back behavior, including predictive back where supported; never replace route infrastructure only to change easing. CupertinoPageRoute, Flutter platform adaptations.

D/E: Hero flights use the navigator overlay. Keep matching tags unique within each route and the destination present on its first animation frame. Keep heroes axis-aligned; account for inherited-theme differences using a compatible flight shuttle where necessary. Verify image readiness, nested navigators, interrupted flights, and transitionOnUserGestures; it is not automatically enabled. Explicitly disable or simplify spatial flights under reduced motion; verify fallback routes too. Hero API.

For approved custom navigation, this transition builder demonstrates stable-child reuse and a live reduced-motion branch. It is not a complete route, nor an interactive-back implementation. Supply existing motion tokens for both route durations; preserve settings, secondary-animation policy, route ownership, and platform behavior at the caller.

import 'package:flutter/widgets.dart';

Widget motionFadeTransition(
  BuildContext context,
  Animation<double> animation,
  Animation<double> secondaryAnimation,
  Widget child,
) {
  if (MediaQuery.disableAnimationsOf(context)) {
    return child;
  }
  return FadeTransition(opacity: animation, child: child);
}

Scrolling and scroll-linked effects

D/E: Keep lazy slivers/list builders and stable entity keys. Prefer sliver geometry for collapsing headers and a localized ScrollController/ScrollPosition listener for position-dependent changes. Verify attachment, metrics availability, and resize before reading dimensions. Never rebuild an entire feed to update one header.

ScrollNotification arrives after build/layout; using it to drive layout causes a one-frame lag. Reserve notifications for appropriate paint effects or observation, and filter depth when nested scrollables matter. ScrollNotification, ScrollController.

Retain the platform's ScrollBehavior: base behavior uses bouncing on iOS/macOS and clamping on Android and other desktop platforms. Do not force bouncing everywhere to imitate Apple. Ballistic simulations can be recreated as metrics change; preserve equivalent motion instead of restarting a timer. Test insertion during scrolling, boundary release, mouse wheel, trackpad, keyboard, and nested sheets. ScrollBehavior physics, Ballistic simulation.

Flutter rendering and frame budget

D: Vsync schedules frame work. Tickers update animation values, then microtasks run; dirty elements build, dirty render objects lay out, compositing bits update, paint records layers, and scene composition feeds the engine. Semantics and finalization follow in the framework lifecycle. Post-frame callbacks still run on the UI isolate; they do not offload CPU work. WidgetsBinding.drawFrame.

E, arithmetic: Use the observed frame interval: 1000 / refreshRateHz milliseconds. At 60/90/120 Hz this is approximately 16.67 / 11.11 / 8.33 ms. Allow headroom; display capability, actual cadence, system load, and thermal conditions differ. Average FPS hides isolated bad frames and input latency.

D/E: UI work and raster work are pipelined. Compare each stage to the frame interval; adding their durations is not a reliable dropped-frame test. Framework build/layout/paint recording is distinct from raster-thread rendering and GPU work. FrameTiming.totalSpan includes additional latency and is not touch-to-photon latency. Do not equate raster duration with a pure GPU timer. FrameTiming, Performance profiling.

Rendering, compositing, and rebuild optimization

MechanismWhat changes / likely costWhen to use; how to verify
Transform / slide / scale / rotationPaint transform; no changed layout footprintUse when surrounding geometry stays fixed; inspect bounds, clipping, hit testing, pixel quality, and raster time. Not guaranteed GPU-only.
Size / alignment / positioned layoutConstraints or geometry may relayout children/siblingsUse when actual flow must change; trace layouts/intrinsics. “Never animate width” is not a valid blanket rule.
Opacity / fadeGroup blending may need offscreen work; engine optimizations varyKeep regions bounded; profile overlapping large surfaces. Per-primitive alpha changes blending semantics when primitives overlap.
ClipShape evaluation and possible antialiasingKeep necessary clipping; not every clip creates a layer. Avoid antiAliasWithSaveLayer unless its output is required.
Blur / backdrop / masks / shadowsSampling, offscreen surfaces, overdraw, effect-area costsNarrow area and radius only if acceptable; compare raster/GPU traces. Prefer ImageFiltered when filtering one child, not its backdrop.
Paths / gradients / custom shadersGeometry, shader execution, and fill costs varyCache stable geometry where useful, limit work/area, and profile actual renderer. “Shader” does not prove compilation jank.
Large images / texturesDecode, upload, cache and external-memory pressureReserve dimensions; decode near display size and selectively precache likely destinations. Inspect first-use versus warm runs.
AnimatedBuilder.child / immutable childrenLess per-tick widget construction/updateExtract expensive invariant content. const does not eliminate required layout, paint, or raster work.
RepaintBoundaryIsolates display-list recording; adds a layerUse at measured differing repaint frequencies. It does not block rebuild/layout or guarantee raster caching; compare memory and timings.
CustomPainter(repaint: ...)Direct paint invalidation without widget rebuild/layoutUse for drawing-only motion; cache stable data, implement shouldRepaint correctly, and keep semantics current.

These are mechanism-based hypotheses, not universal cost rankings. Sources: Flutter performance best practices, RepaintBoundary, BackdropFilter, AnimatedBuilder.

Profile saveLayer and offscreen buffers rather than removing effects blindly. For overlapping backdrop filters, do not reuse a backdrop key that combines effects incorrectly. Platform views, video, maps, and WebViews need platform-specific checks under transform, opacity, clipping, snapshotting, and Hero flight.

Impeller and version-dependent advice

D/P, documentation checked 2026-08-27: iOS supports Impeller only. Android API 29+ with Vulkan uses Impeller by default; older or unsupported devices use a legacy OpenGL fallback. The official page states macOS, Linux, and Windows default to Impeller from Flutter 3.47, with temporary opt-outs. Web uses Skia, not Impeller. Verify actual runtime/backend and SDK; never copy these defaults into an older project without checking. Impeller availability.

Impeller precompiles its engine shader set; it does not eliminate Dart, geometry, image, filter, compositing, or GPU bottlenecks. Legacy SkSL capture/warm-up is not a universal first-animation fix and does not apply as an Impeller shader remedy. Trace first-use stalls before naming their cause. Custom shader loading/execution still needs profiling. Rendering performance.

For web, record CanvasKit/skwasm selection, JS/Wasm build, browser capabilities, and deployment headers. Renderer worker support does not turn Dart compute into a background isolate. Do not revive obsolete HTML-renderer advice. Flutter WebAssembly.

Dart hot paths and resource efficiency

D/E: async, Future, microtasks, and post-frame callbacks do not move synchronous computation off the current isolate. Keep parsing, large sorts, image processing, logging, and collection copying out of frame callbacks. Use asynchronous I/O directly; move measured CPU-heavy work to an isolate when native transfer/spawn cost is worthwhile. Do not spawn a worker for each tick. Flutter compute runs on the main thread on web; use an appropriate web-worker design only when needed. Dart concurrency, Flutter isolates.

Correlate allocation/GC events with bad frames before pooling objects or eliminating ordinary widget allocation. Reuse expensive stable paths/data, bound caches, and inspect retained objects and external image memory. GC cannot reclaim unwanted objects that remain referenced by listeners, closures, or caches. Native debug JIT behavior is not native profile/release AOT behavior; web compilation and profiling differ. DevTools Memory, Flutter build modes.

Architecture and motion tokens

E: Reuse the project's design-system accessors and state-management boundaries. Keep semantic target state in the established owner; keep controllers, tickers, gesture progress, and interpolation in the presentation lifecycle. Do not send 120 progress events per second through domain state merely to repaint a transform.

Centralize repeated durations, curves, spring descriptions, and reduced-motion alternatives in existing tokens or a ThemeExtension. Name tokens by purpose, such as feedback, content change, or surface settle. Document units and tuning intent; component-specific values are acceptable for distinct geometry or interaction, not unexplained copies of arbitrary constants.

Expose semantic primitives only when reused. Coordinate related properties through one controller and intervals/tween sequences; do not create a global motion manager, a controller per property, or sequential timers for a single transition. Keep meaningful loading state truthful instead of inventing progress.

Accessibility and reduced motion

D/P/E: Observe MediaQuery.disableAnimationsOf(context) through the project's context accessor where one exists. React both on entry and when the preference changes while motion is active. AnimationBehavior.normal reduces duration; it is not a complete policy for large spatial effects. Unbounded controllers default to preserve, and custom animateWith simulations need explicit handling. MediaQuery preference, AnimationBehavior.

Replace large travel/zoom/parallax/depth with immediate state or a restrained fade when appropriate; stop decorative repetition instead of making it flash faster. Preserve direct finger-following and ordinary scrolling. Never imply Flutter's single flag exposes every Apple preference independently. Keep status, selection, validation, and progress understandable through persistent content and semantics. Apple accessibility guidance.

At zero opacity, descendants can still receive pointer hits. Manage visibility, IgnorePointer, focus eligibility, and semantics separately; rebuild interaction gates only at meaningful state changes. Do not disable all interaction for the duration of every fade. Check duplicate semantics/focus when outgoing and incoming children coexist. Preserve keyboard activation, screen-reader order, text scaling, RTL, and sufficient contrast without relying on motion. FadeTransition hit testing.

Lifecycle and resource management

D/E: Create controllers outside build, normally in initState; update configuration in lifecycle hooks and dispose controllers before super.dispose. Use SingleTickerProviderStateMixin for one ticker over the state's lifetime, and TickerProviderStateMixin for multiple tickers. Remove listeners from externally owned listenables without disposing their owner's object. AnimationController lifecycle.

TickerMode mutes widget-aware tickers but elapsed time still advances; it is not a paused simulation clock. Offstage alone does not stop animations, focus, or CPU work. Explicitly manage decorative/offscreen activity and app background/resume policy, including retained routes and list children. Use the existing lifecycle observer or AppLifecycleListener, and dispose it. TickerMode, Offstage.

Canceled TickerFutures may never complete normally. If awaiting animation completion, use .orCancel with a specific TickerCanceled handler, then check mounted/current operation before a UI side effect. Do not hide unrelated exceptions or let stale completion commit an outdated destination. A business operation must complete correctly even if its animation is canceled. TickerFuture.orCancel.

Reference pattern: spring handoff and local rebuilding

This reusable helper takes a project-owned spring and an explicit accessibility decision. It captures velocity before replacement, preserves sign, rejects invalid inputs, and settles immediately for reduced motion. Use an unbounded controller for physical displacement; use a bounded controller only when clipping the trajectory is intentional. snapToEnd is available in the validation SDK; verify support in older SDKs.

import 'package:flutter/animation.dart';
import 'package:flutter/physics.dart';

void settleMotion({
  required AnimationController controller,
  required double target,
  required SpringDescription spring,
  required bool reduceMotion,
  double? velocity,
}) {
  final handoffVelocity = velocity ?? controller.velocity;
  if (!target.isFinite ||
      target < controller.lowerBound ||
      target > controller.upperBound ||
      !handoffVelocity.isFinite ||
      !spring.mass.isFinite ||
      spring.mass <= 0 ||
      !spring.stiffness.isFinite ||
      spring.stiffness <= 0 ||
      !spring.damping.isFinite ||
      spring.damping <= 0) {
    throw ArgumentError('Motion requires a valid target and settling spring.');
  }
  if (reduceMotion) {
    controller.value = target;
    return;
  }
  controller.animateWith(
    SpringSimulation(
      spring,
      controller.value,
      target,
      handoffVelocity,
      snapToEnd: true,
    ),
  );
}

double progressVelocity(double pixelsPerSecond, double signedTravel) {
  if (!pixelsPerSecond.isFinite ||
      !signedTravel.isFinite ||
      signedTravel == 0) {
    throw ArgumentError('Velocity and nonzero travel must be finite.');
  }
  final result = pixelsPerSecond / signedTravel;
  if (!result.isFinite) {
    throw ArgumentError('Normalized velocity must be finite.');
  }
  return result;
}

For a progress controller, pass progressVelocity(details.velocity.pixelsPerSecond.dy, signedTravel) on release. For a pixel-displacement controller, pass the signed pixel velocity directly. For a programmatic retarget, omit velocity. The target-selection policy belongs to the interaction, not this helper. On drag cancellation, do not treat cancellation as a release-to-dismiss.

In the host State, create the unbounded controller once with vsync: this, stop it when a drag wins, and dispose it. On reduced-motion changes, call the helper with the current semantic target, not a stale endpoint. Rendering can remain local with this complete wrapper:

import 'package:flutter/widgets.dart';

class MotionTranslation extends StatelessWidget {
  const MotionTranslation({
    required this.offset,
    required this.child,
    super.key,
  });

  final Animation<double> offset;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: offset,
      child: child,
      builder: (context, child) {
        return Transform.translate(
          offset: Offset(0, offset.value),
          child: child,
        );
      },
    );
  }
}

The wrapper expects logical pixels, not normalized progress. It preserves the child's layout footprint; callers must choose appropriate clipping and hit regions. Add a RepaintBoundary around invariant expensive content only if repaint evidence justifies it. For a drawing-only implementation, pass the controller to CustomPainter(repaint: controller) and read its value in paint; return true from shouldRepaint when relevant non-listenable inputs change.

Profiling workflow and debugging jank

E: Record SDK/revision, build mode, device/OS, renderer, observed refresh rate, scenario, content sizes, and cold/warm state. Use physical native devices in profile mode (flutter run --profile -d DEVICE_ID); use browser performance tools for web with representative deployment settings. Debug mode is useful for correctness and inspection, not production frame claims. Flutter profiling, DevTools Performance.

Symptom / questionInspectNext experiment
UI-stage spikesTimeline, CPU samples, tracked builds/layouts, intrinsicsLocalize rebuilding or move identified synchronous computation; compare the same trace scenario.
Raster-stage spikesPaint/layers, effect area, image sizes, GPU/platform tools where availableTemporarily simplify one expensive effect or image to isolate cause; restore required appearance.
First-use stutterCold vs warm trace; decode/upload, initialization, renderer eventsPrepare only likely content; never assume every first stall is shader compilation.
Persistent jankRepeated identical interaction; both stages and device loadFind sustained work, overdraw, concurrent motion, or thermal constraints.
Scroll-linked lagNotification timing, broad listeners, sliver/layout workMove layout to appropriate scroll/geometry ownership; keep paint effects localized.
Route-transition stallsBoth route trees, Hero images, route construction, platform viewsReserve geometry, reduce duplicate active work, inspect first-use content.
Allocation/GC-correlated stallsMemory allocation traces, GC events, external memoryRemove measured churn or retained resources; rerun long repetition.
Excessive repaintInspector repaint visualization / repaint boundary diagnosticsTest one boundary at differing repaint frequencies; compare memory and raster cost.

Use Inspector for tree/constraint/repaint investigation, not FPS certification. Enable enhanced build/layout/paint tracing only to locate the cause; it adds overhead. Export traces and compare without intrusive diagnostics. Flutter Inspector.

Capture first-use and repeated runs separately. Compare UI/raster p50/p95/p99, worst frames, over-budget stage counts, memory, and visible interaction continuity using the same device/scenario. Stage counts are not exact displayed-drop counts. Do not claim a speedup from one favorable average or a simulator video. Report missing physical-device, web, or high-refresh validation explicitly.

Testing motion

D/E: In widget tests, start the ticker with pump() and advance with deliberate pump(Duration(...)) calls. One pump advances one frame, not all intervening frames. Assert meaningful intermediate and terminal states. Use bounded pumpAndSettle only for finite motion; repeating indicators, queued work, and benchmark frame policies can prevent settling. Widget testing, pumpAndSettle.

  • Test retarget/reverse from a nonterminal position; signed positive/negative/zero release velocities; cancellation; re-grabbing; and disposal while ticking. Check position continuity and initial simulation velocity, not only eventual arrival.
  • Inject reduced motion at startup and during an active spring or route flight. Assert semantic state, focus, and operability remain correct; reset test overrides afterward.
  • Test zero/changed extent, RTL, large text, nested navigation/scrolling, back-gesture cancellation, and rapid child replacement where relevant. Include keyboard/pointer alternatives to touch.
  • Use goldens for a few deterministic visual states with controlled fonts/images; a golden does not establish timing or native feel. Avoid wall-clock sleeps and network assets in widget tests.
  • Use an integration/performance scenario on representative hardware for pacing. Widget tests validate behavior, not GPU throughput, battery, thermal behavior, or touch-to-photon latency.

Anti-patterns: mechanism, scope, alternative, detection

PatternWhy / when harmfulBetter alternative; detection
Screen-wide update every tickRebuilds expensive unrelated content during frequent motionLocal transition/listenable and stable child; track builds/CPU.
Parsing, sorting, I/O side effects in buildersBlocks or repeats work on frame pathsPrecompute/load outside interpolation; inspect timeline and callback callers.
Controllers created in build / undisposed listenersRestarts motion or retains ticking resourcesLifecycle ownership; repeated mount/unmount and leak/ticker checks.
Restarting from zero or dropping release signTeleportation or wrong-way settling during interruptionCapture current value/velocity and signed conversion; gesture tests.
Global input lock until animation completionPrevents cancellation and delays intentLocal duplicate-action guard only when correctness needs it; rapid-input tests.
Many loops / controllers in retained list childrenHidden CPU, battery, and allocation workAnimate meaningful visible content, stop decorative loops; offscreen/profile tests.
Fullscreen animated blur / stacked translucent surfacesLarge offscreen regions and repeated samplingBound effects or simplify with approval; raster/GPU comparison.
Blind clips, opacity layers, saveLayer, boundariesExtra work/layers without benefitKeep only required effects/isolation; inspect layers, memory, and timing.
Recreating complex paths/images per framePaint, decode/upload, or allocation pressureReuse stable data, size assets; paint/memory traces.
Long decorative or identical cross-platform motionSlows frequent actions or violates input/navigation expectationsPurposeful tokens and platform behavior; usability and accessibility review.
Debug-only FPS checks / universal 16 ms budgetMisrepresents production and high-refresh performanceProfile actual devices/cadence; compare stage distributions.
Folklore-driven optimizationChanges code without finding the expensive stageBaseline, one mechanism change, repeat measurement; retain evidence.

Code review and completion checklist

  • Purpose, semantic target ownership, platform conventions, and reduced-motion alternatives are explicit.
  • Controller/ticker/listener lifecycle, background policy, cancellation futures, and stale callbacks are correct.
  • Gesture direction/units, velocity continuity, bounds, reversal, cancellation, and changed geometry are handled.
  • Route identity/results, nested navigators, back gestures, browser history, and Hero state remain intact.
  • Rebuild, layout, paint, compositing, raster, allocation, and image costs are distinguished; risks are not mislabeled measured bugs.
  • Motion values reuse tokens; business correctness does not depend on visual completion; no unnecessary motion infrastructure was added.
  • Focus, semantics, invisible hit targets, reduced motion, RTL, text scale, and non-touch input were checked.
  • Analyzer and relevant behavioral tests passed, or failures are disclosed. Performance claims include profile evidence and exact tested coverage.

For a finding, report: classification → affected interaction/code → observed mechanism/evidence → smallest correction → verification. If no profile is available, say “behavior checked; performance unmeasured.”

Curated source index

The inline references above connect individual rules to primary sources. These core sources establish the broader engineering model; Apple-to-Flutter mappings and the workflow are engineering synthesis, not vendor certification.

Source / organizationWhat it establishes
Flutter architectural overview — FlutterFramework/engine responsibilities, trees, rendering architecture.
Frame lifecycle — Flutter APIOrdering of animation, build, layout, paint, composition, semantics, and cleanup.
Animation and motion widgets — FlutterFramework primitives and implementation choices.
Gestures — FlutterPointer versus gesture recognition and gesture-arena behavior.
Physics simulation recipe — FlutterGesture-to-simulation implementation mechanics; not a complete production interaction.
Performance best practices — FlutterBuild/layout costs, lazy construction, clipping, opacity, and offscreen layers.
Impeller — FlutterRenderer availability, platform qualifications, and shader strategy.
Performance view — Flutter DevToolsFrame analysis, UI/raster diagnosis, trace instrumentation.
Memory view — Flutter DevToolsAllocation, GC, retained objects, and external memory investigation.
Concurrency — DartEvent loop, isolates, message passing, and web constraints.
Motion — Apple HIGPurpose, feedback, responsiveness, and interruptible interaction.
Accessibility — Apple HIGMotion alternatives, sensitivity, and accessible interaction.
Animate with springs — Apple WWDC23Position/velocity continuity, spring tuning, perceived versus settling duration.
Testing system accessibility features — AppleDevice-level accessibility checks beyond source inspection.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.