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.
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!