Dart Flutter Article

Effective Animations in Flutter

How to create efficient and smooth animations in Flutter by optimizing refresh rates, using RepaintBoundary, managing Canvas effectively, and leveraging custom tickers. This guide provides key strategies to minimize performance issues and craft fluid animations without overwhelming your system.
Plague Fox 4 min read
Effective Animations in Flutter

Creating efficient and smooth animations in Flutter requires a thoughtful approach to performance optimization. In this article, we will explore the key principles that will help you create animations without overloading the system and ensuring the smoothest possible user experience. We will discuss the use of tickers, RepaintBoundary, working with Canvas, and other important aspects to achieve the best results.

0:00
/0:38

1. Refresh Rate and FPS Limitation

The first thing to remember when developing animations in Flutter is to work with the display's refresh rate. For example, on a MacBook with macOS, the maximum refresh rate is 120 frames per second (FPS). Flutter automatically adjusts the refresh rate of your animations according to the device display, allowing for the smoothest animation without unnecessary CPU load.

2. Avoid Multiple setState and AnimatedBuilder

Calling multiple setState and overusing widgets like AnimatedBuilder can significantly decrease the performance of your application. These methods constantly rebuild the widget tree, leading to high resource costs. Instead, it is better to manage rendering directly, avoiding the need to change the widget structure.

3. Use RepaintBoundary to Isolate Repaints

To optimize animations, it is crucial to use RepaintBoundary. This widget isolates repainting to only those parts of the screen that actually change, preventing unnecessary reprocessing of the entire screen. Widgets that frequently update together should be wrapped in RepaintBoundary.

It's also worth noting that RepaintBoundary is already integrated into RenderBox, and you can use it directly to manage repaints efficiently, especially if you follow the principle of not always needing to call setState.

4. Never Change the Widget Tree Structure

When creating animations, avoid modifying the structure of the widget tree (elements). Any changes can lead to delays and performance degradation. Always work with existing elements without adding or removing them unnecessarily.

5. Use a Custom Ticker

For more precise control over animations, you can use a custom ticker through the Ticker class. This allows direct interaction with Flutter and frame rate control, significantly simplifying animation management and reducing rendering overhead.

6. Mark Render Object for Repainting

Instead of using setState, you can directly mark the RenderObject (of LeafRenderObjectWidget) as needing repainting with method markNeedsPaint. This helps avoid rebuilding the entire widget tree and focuses only on the elements that need updates.

7. Efficient Use of Canvas

Working with Canvas in Flutter allows you to achieve high performance when drawing complex scenes. Here are a few recommendations:

  • Reusing Paints and Shaders is a pretty important thing.
  • Instead of using loops to draw a large number of objects, use methods such as drawAtlas, drawVertices, drawPoints, which draws multiple objects more efficiently in one pass.
  • Proper use of Layers (e.g., saving and restoring context) helps optimize working with Canvas and avoids unnecessary repaints.
  • Understand that widgets are a configuration, not a presentation. Also, canvas and paint are not drawings on the screen; they are reusable instructions for drawing.

Imagine you need to render 100 enemies on the screen. How would you do it?
A beginner might do something like this:

final paint = Paint();
for (final enemy in enemies)
  canvas.drawImageRect(
    image,
    enemyImage,
    enemyPosition(enemy),
    paint,
  );

Right?

A smart approach would look like this:

final paint = Paint()
  ..filterQuality = FilterQuality.none
  ..isAntiAlias = false;

final transforms = enemies.map<RSTransform>(transformEnemy).toList(growable: false);

final rects = enemies.map<Rect>(rectEnemy).toList(growable: false);

canvas.drawAtlas(atlas, transforms, rects, null, null, null, paint);

Using drawAtlas here is significantly more efficient than looping through each enemy. This approach helps to reduce the overhead of repetitive draw calls and makes the rendering process much smoother, particularly when dealing with a large number of objects.

You should always consider and understand what exactly you are doing, think about layers, instructions, GPU calls and efficient resource utilization.
Don't do everything "head-on" as you usually do with logic calculated on the CPU.
And also always add and display metrics and graphs on the time of calculating the frame state update, canvas formation and subsequent rendering.

8. Metrics for everything

Use stopwatch for this.
You can also consider that the canvas rendering stage lasts from the moment the render method is completed until the next event loop tick, to record this time you can use postFrameCallback.

Something like that:

final stopwatch = Stopwatch()..start();
double elapsed = .0;

void update() {
  elapsed = stopwatch.elapsedMilliseconds;
  // ...
  elapsed = stopwatch.elapsedMilliseconds;
}

void render(Canvas canvas) {
  elapsed = stopwatch.elapsedMilliseconds;
  // ...
  elapsed = stopwatch.elapsedMilliseconds;
  SchedulerBinding.addPostFrameCallback((Duration timeStamp) {
    elapsed = stopwatch.elapsedMilliseconds;
  });
}

9. Use Fragment Shaders

Fragment shaders allow you to create complex visual effects with high performance. With shaders, you can achieve impressive visual effects at the maximum frame rate. Unfortunately, Flutter currently does not support vertex shaders, which limits the ability to create complex 3D scenes, but fragment shaders are already a powerful tool for creating graphic animations.

10. Start with CustomPainter

If you are just starting out with animations in Flutter, a good starting point is to use CustomPainter and pass an AnimationController to the repaint argument. This is a more efficient approach than using setState for updates at every tick.

11. RepaintBoundary is Your Friend

To isolate repaints and prevent excessive screen updates, it is recommended to use the RepaintBoundary class. This is a key tool for effectively managing the refresh rate and reducing the system load when creating complex animations.

But again, if all your animation is on canvas and without setStates, you use your own render object, then you donโ€™t need to wrap something in a repaint boundry separately, the render object can do it on its own.

Conclusion

Creating effective animations in Flutter requires a careful approach to optimization, minimizing widget tree rebuilds, and using tools like RepaintBoundary and Ticker to manage rendering. It is also important to understand how to use Canvas and shaders to achieve the best results. By following these recommendations, you will be able to create truly smooth and impactful animations without overloading the system.

For a more detailed study of animation optimization approaches, you can check out my package on GitHub, which has a similar structure to Flame but is more adequate for certain tasks: GitHub โ€“ PlugFox/repaint.

I hope this article helps you create truly impressive and smooth animations in Flutter!

Share
Comments
More from Plague Fox
Microbenchmarks are experiments
Dart Flutter Article

Microbenchmarks are experiments

Benchmarks are not just about numbersโ€”they are experiments that need interpretation. This post dissects a Dart vs JavaScript microbenchmark, illustrating why cool animations often mask the real value: insightful analysis. Numbers without context are just as meaningful as numerology
Vyacheslav Egorov 12 min read

Plague Fox

Engineer by day, fox by night. Talks about Flutter & Dart.

Great! Youโ€™ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Plague Fox.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.