Flutter Laggy Animations: How Not To setState

Tomek Polański
Flutter Community
Published in
3 min readMar 26, 2019

--

Creating animations in Flutter is really fun and easy, but there is one bad practice that might make those animations laggy: setState.

Bad Practice: AnimationController and setState

When using AnimationController in a StatefulWidget, you might be tempted to do the following:

void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)
..addListener(() => setState(() {}));
}

To animate a Placeholder from left to right by 100 pixels:

Transform.translate(
offset: Offset(100 * _controller.value, 0),
child: Placeholder(key: Key('animated')),
),

This will animate the Placeholder by it will also rebuild all other widgets in the StatefulWidget:

Column(
children: [
Placeholder(key: Key('1')), // rebuilds
Placeholder(key: Key('2')), // rebuilds
Placeholder(key: Key('3')), // rebuilds
Transform.translate( // rebuilds
offset: Offset(100 * _controller.value, 0),
child: Placeholder(key: Key('animated')),
),

],
),

This is the build timeline[1]:

AnimationController and setState

Those Placeholder widgets are light but in a production application, with bigger widgets, there might be a performance issue.

Good Practice: AnimatedBuilder without setState

First, you need to remove ..addListener(() => setState(() {})):

void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
// No
addListener(...)
}

Then wrap your transition into an AnimatedBuilder:

AnimatedBuilder(
animation: _controller,
builder: (_, child) {

return Transform.translate(
offset: Offset(100 * _controller.value, 0),
child: child,
);
},
child: Placeholder(key: Key('animated')),
),

That’s all. Because we’ve removed setState the StatefulWidget is not rebuilding. The only widget that will rebuild is AnimatedBuilder:

AnimatedBuilder without setState

Bad Practice: Opacity with AnimatedBuilder

You could do the same trick with changing opacity and using AnimatedBuilder:

AnimatedBuilder(
animation: _controller,
builder: (_, child) {
return Opacity(
opacity: _controller.value,
child: child,
);

},
child: Placeholder(key: Key('animated')),
),
Opacity with AnimatedBuilder

Changing opacity is a quite expensive animation, that’s why there is a better way of doing that.

Good Practice: Opacity with FadeTransition

There is a special opacity transition that is optimized for performance:

FadeTransition(
opacity: _controller,

child: Placeholder(key: Key('animated')),
),

With this approach, the build is never called:

Opacity with FadeTransition

Build phase is usually placed between Animation and Layout phase — here you can notice now it’s gone.

Conclusion

Try always to avoid using setState with AnimationController like this:

Don’t use setState like this

Additionally, check out transitions.dart within Flutter framework to see what other optimised transition you can use.

All the source code for those examples can be found here.

Thanks Frederik Schweiger for the help with the post

Sidenote:

If you wonder why Opacity and FadeTransition widgets are not visible in the timeline, it is because they are not backed up by Elements but create RenderObjects directly for performance improvement.

Remember that Build phase comes from Elements.

[1] To enable Build phase profiling in the timeline, enable debugProfileBuildsEnabled flag in your Flutter main method

--

--

Tomek Polański
Flutter Community

Passionate mobile developer. One thing I like more than learning new things: sharing them