Skip to content

Fix: CurvedAnimation instances not disposed (fixes #155)#166

Open
winter-cw wants to merge 1 commit into
gskinner:mainfrom
winter-cw:fix/155-visibility-detector-dependency
Open

Fix: CurvedAnimation instances not disposed (fixes #155)#166
winter-cw wants to merge 1 commit into
gskinner:mainfrom
winter-cw:fix/155-visibility-detector-dependency

Conversation

@winter-cw
Copy link
Copy Markdown

Fix: CurvedAnimation instances not disposed (fixes #155)

Reported by @raulmabe-labhouse in #155

Problem

EffectEntry.buildAnimation() creates a new CurvedAnimation on every call:

return CurvedAnimation(
  parent: controller,
  curve: Interval(beginT / ttlT, endT / ttlT, curve: curve ?? this.curve),
);

CurvedAnimation is a disposable object — it registers itself as a listener on the parent AnimationController and must be explicitly .dispose()d to unregister. However, buildAnimation() is called from Effect.build(), which is invoked during _AnimateState.build(). This means a new CurvedAnimation is allocated on every widget rebuild, and none of them are ever disposed.

The _AnimateState.dispose() method only disposes the AnimationController — disposing the controller does not dispose child CurvedAnimation instances attached to it.

How we found it

We integrated Dart's leak_tracker package into our production Flutter app with collectStackTraceOnStart: true enabled, which captures creation stack traces for every tracked object. During a typical user session navigating through multiple screens, we observed CurvedAnimation as one of the top leaked types, with all creation stack traces pointing to EffectEntry.buildAnimation in flutter_animate.

In a single exploration session across ~15 screens, we measured 62 leaked CurvedAnimation instances from flutter_animate alone. After applying this fix, that number dropped to 0 — the remaining CurvedAnimation leaks in the report were all from Flutter framework internals (overscroll glow, FAB transitions, implicit animations).

Fix

Replace CurvedAnimation with CurveTween + Animation.drive():

return controller.drive(
  CurveTween(curve: Interval(beginT / ttlT, endT / ttlT, curve: curve ?? this.curve)),
);

Both produce the same Animation<double> applying the same curve. The difference:

  • CurvedAnimation(parent: controller, curve: ...) — creates a disposable object that registers as a listener on the parent controller. Must be explicitly disposed.
  • controller.drive(CurveTween(curve: ...)) — returns a non-disposable _AnimatedEvaluation that evaluates the tween lazily on each frame. No listener registration, no disposal needed.

Since buildAnimation() is called inside build() — where there is no corresponding lifecycle hook to dispose the result — CurveTween + .drive() is the correct pattern. This is consistent with Flutter's own documentation recommending .drive() over CurvedAnimation when the animation is created in contexts without disposal management.

Impact

Every Animate widget with effects (fade, slide, scale, etc.) leaks one CurvedAnimation per effect per widget lifecycle. In apps that use flutter_animate extensively (143 files in our codebase), this adds up to hundreds of undisposed objects during a typical user session, holding references to animation controllers and preventing proper garbage collection.

Tests

All 52 existing tests pass with no changes required — the fix is API-compatible and produces identical animation behavior.

🤖 Generated with Claude Code

Replace CurvedAnimation with CurveTween + Animation.drive to avoid
creating disposable objects during the build phase that are never
disposed, which causes memory leaks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CurvedAnimations not getting disposed

1 participant