Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance

If you’ve ever tried to add animations to your Flutter app, you know there’s a fine line between delightful user experience and watching your app’s performance graph take a nosedive. After spending countless hours debugging janky animations and memory …


This content originally appeared on DEV Community and was authored by Shanu Kumawat

If you've ever tried to add animations to your Flutter app, you know there's a fine line between delightful user experience and watching your app's performance graph take a nosedive. After spending countless hours debugging janky animations and memory leaks, I've compiled my hard-earned lessons into this guide.

The Animation Problem

Flutter offers incredible animation capabilities out of the box, but there's a catch: poorly implemented animations can destroy your app's performance. The biggest culprits I've encountered are:

  1. Rebuilding entire widget trees during animation
  2. Running too many simultaneous animations
  3. Using complex animations on low-end devices
  4. Failing to dispose of animation controllers

These issues become especially problematic when you're trying to create reusable animation components. Let's fix that.

Starting With the Basics: Animation Controllers

Every good Flutter animation starts with proper controller management. Here's my go-to pattern for creating a reusable, performance-friendly animated widget:

class OptimizedAnimatedWidget extends StatefulWidget {
  final Widget child;
  final Duration duration;

  const OptimizedAnimatedWidget({
    Key? key,
    required this.child,
    this.duration = const Duration(milliseconds: 300),
  }) : super(key: key);

  @override
  _OptimizedAnimatedWidgetState createState() => _OptimizedAnimatedWidgetState();
}

class _OptimizedAnimatedWidgetState extends State<OptimizedAnimatedWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    );
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose(); // This is crucial!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Transform.scale(
          scale: _animation.value,
          child: child,
        );
      },
      child: widget.child, // This is our performance trick!
    );
  }
}

The secret sauce here is passing the child to AnimatedBuilder. This prevents Flutter from rebuilding the child on every animation frame, which is a common performance killer.

Technique #1: RepaintBoundary for Complex Animations

When your animations involve complex widgets, wrap them in a RepaintBoundary:

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _animation,
    builder: (context, child) {
      return Transform.scale(
        scale: _animation.value,
        child: RepaintBoundary(child: child),
      );
    },
    child: widget.child,
  );
}

This creates a new "layer" for Flutter's rendering engine, preventing the entire widget tree from repainting on each animation frame.

Technique #2: Custom Tween Classes for Reusability

To make animations truly reusable, I create custom Tween classes:

class ShakeTween extends Tween<double> {
  ShakeTween({double begin = 0.0, double end = 10.0})
      : super(begin: begin, end: end);

  @override
  double lerp(double t) {
    // Custom shake animation
    if (t < 0.25) {
      return -sin(t * 4 * pi) * end! * (t * 4);
    } else if (t < 0.75) {
      return sin((t - 0.25) * 4 * pi) * end! * (0.75 - t) * 1.33;
    } else {
      return -sin((t - 0.75) * 4 * pi) * end! * (1 - t) * 4;
    }
  }
}

Now I can reuse this shake animation anywhere:

final Animation<double> _shakeAnimation = ShakeTween().animate(_controller);

Technique #3: Composable Animation Widgets

For truly reusable animations, I build composable widgets that can be stacked:

class FadeScale extends StatelessWidget {
  final Widget child;
  final Duration duration;
  final bool isActive;

  const FadeScale({
    Key? key,
    required this.child,
    this.duration = const Duration(milliseconds: 200),
    this.isActive = true,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      tween: Tween<double>(begin: 0.0, end: isActive ? 1.0 : 0.0),
      duration: duration,
      curve: Curves.easeOut,
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Transform.scale(
            scale: 0.8 + (value * 0.2),
            child: child,
          ),
        );
      },
      child: child, // This prevents unnecessary rebuilds
    );
  }
}

I can now use this anywhere in my app:

FadeScale(
  isActive: _isVisible,
  child: MyExpensiveWidget(),
)

Performance Testing Tips

After implementing these patterns, I always test animations on real devices (especially low-end Android phones) using the following:

  1. Timeline View: Enable "Track widget builds" in Flutter DevTools to see if widgets are rebuilding unnecessarily.

  2. Performance Overlay: Add MaterialApp(showPerformanceOverlay: true) to check for dropped frames.

  3. Memory Profiling: Watch memory usage during animations to catch leaks from undisposed controllers.

Real-World Example: A Reusable "Heart Beat" Animation

Here's a complete example of a performance-optimized, reusable heart beat animation I use in production:

class HeartBeat extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final bool isAnimating;

  const HeartBeat({
    Key? key,
    required this.child,
    this.duration = const Duration(milliseconds: 1500),
    this.isAnimating = true,
  }) : super(key: key);

  @override
  _HeartBeatState createState() => _HeartBeatState();
}

class _HeartBeatState extends State<HeartBeat> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    );

    _scaleAnimation = TweenSequence<double>([
      TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.2), weight: 10),
      TweenSequenceItem(tween: Tween<double>(begin: 1.2, end: 1.0), weight: 10),
      TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.15), weight: 10),
      TweenSequenceItem(tween: Tween<double>(begin: 1.15, end: 1.0), weight: 10),
      TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.0), weight: 60),
    ]).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));

    if (widget.isAnimating) {
      _controller.repeat();
    }
  }

  @override
  void didUpdateWidget(HeartBeat oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.isAnimating != oldWidget.isAnimating) {
      if (widget.isAnimating) {
        _controller.repeat();
      } else {
        _controller.stop();
      }
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _scaleAnimation,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnimation.value,
          child: child,
        );
      },
      child: widget.child,
    );
  }
}

Conclusion

After implementing these techniques in multiple apps, I've seen dramatic performance improvements, especially on older devices. The key takeaways are:

  1. Prevent unnecessary rebuilds by using the child parameter in AnimatedBuilder
  2. Create custom Tweens for complex motion
  3. Use RepaintBoundary for complex widgets
  4. Always dispose your controllers
  5. Test on real, low-end devices

What animation challenges have you faced in your Flutter projects? I'd love to hear about them in the comments below!


This content originally appeared on DEV Community and was authored by Shanu Kumawat


Print Share Comment Cite Upload Translate Updates
APA

Shanu Kumawat | Sciencx (2025-03-04T12:06:04+00:00) Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance. Retrieved from https://www.scien.cx/2025/03/04/creating-custom-reusable-animations-in-flutter-that-dont-kill-performance/

MLA
" » Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance." Shanu Kumawat | Sciencx - Tuesday March 4, 2025, https://www.scien.cx/2025/03/04/creating-custom-reusable-animations-in-flutter-that-dont-kill-performance/
HARVARD
Shanu Kumawat | Sciencx Tuesday March 4, 2025 » Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance., viewed ,<https://www.scien.cx/2025/03/04/creating-custom-reusable-animations-in-flutter-that-dont-kill-performance/>
VANCOUVER
Shanu Kumawat | Sciencx - » Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/03/04/creating-custom-reusable-animations-in-flutter-that-dont-kill-performance/
CHICAGO
" » Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance." Shanu Kumawat | Sciencx - Accessed . https://www.scien.cx/2025/03/04/creating-custom-reusable-animations-in-flutter-that-dont-kill-performance/
IEEE
" » Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance." Shanu Kumawat | Sciencx [Online]. Available: https://www.scien.cx/2025/03/04/creating-custom-reusable-animations-in-flutter-that-dont-kill-performance/. [Accessed: ]
rf:citation
» Creating Custom, Reusable Animations in Flutter That Don’t Kill Performance | Shanu Kumawat | Sciencx | https://www.scien.cx/2025/03/04/creating-custom-reusable-animations-in-flutter-that-dont-kill-performance/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.