Splitting widgets to methods is an antipattern
It has been almost 6 months since I wrote an article on cleaning up Flutter UI code.
The article had several tips on how to organize your Flutter UI code for less clutter and more readability. And it’s still a quite popular article. However, there was this one tip there that advocated doing something that turned out to be a not that good thing to do.
To escape the Lisp-y bracket hell, I advocated for splitting long build methods into multiple separate smaller methods. The following code sample is entirely nonsensical, but we’ll pretend that this is a snippet that does something really useful.
So, for example, if we have a widget that looks something like this:
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
),
],
);
}
}
Looking at the code, you might get a sense that the nesting level gets a little crazy.
And they do. It would be awesome to reduce the indentation level a little bit. Since widgets can be a little boilerplatey, the first solution that comes into mind is to split the nesting part into a separate method.
Our first intuition might end us having something that looks like this:
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
Widget _buildNonsenseWidget() {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
// The deeply nesting widget is now refactored into a
// separate method and we have a cleaner build method. Yay!
_buildNonsenseWidget(),
],
);
}
}
Problem solved, right? Time to call it a day and go home!
Well, not quite.
The problem with splitting widgets into methods
At first glance, splitting long build methods into small functions makes perfect sense. And you can certainly see this pattern used in the wild a lot. It’s also in heavy use in our codebase at Reflectly - and believe me, we have a bunch of UI code.
Now, 6 months later after my initial article, I’m here to tell you that you shouldn’t do this. If you picked up this bad habit after reading my article, you might be a little pissed at me right now. And you’re welcome. This is what friends do for each other. I’m very sorry.
The issue with this was first brought to my attention by Wm Leler in the comment section of the aforementioned article.
For those that don’t already know, Wm is a developer advocate for Flutter.
Some of you that read Wm’s comment will have an a-ha moment right now. However, some of you, including me initially, won’t. And that’s fine - we’ll learn what is going on here.
So what’s the problem, really?
Whenever the value of _counter
changes, the framework calls the build
method.
This triggers our widget to rebuild itself.
The problem is that _buildNonsenseWidget()
gets called every time the value of _counter
changes - which ends up rebuilding the widget tree over and over again.
Rebuilding for nothing
In this case, there’s no reason to rebuild that particular widget tree.
The widget tree returned by _buildNonsenseWidget()
is stateless by nature - we only need to build it once.
Sadly, because the widget tree is built by the _buildNonsenseWidget()
method, the Flutter framework rebuilds it every time when the parent widget rebuilds.
Essentially, we’re wasting precious CPU cycles in rebuilding something that doesn’t need to be rebuilt. This happens because from the framework’s perspective, there’s no difference between a long-ass build method and a build method split into multiple smaller methods. Mind you, this is only a simple example - this has a more significant impact on more complex apps.
Splitting long build methods - revisited
The solution for this one is relatively simple, although it results in a couple of extra lines of code. Instead of splitting build methods into smaller methods, we split them into widgets - StatelessWidgets, that is.
When we refactor the previous example, we’ll end up with this:
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
// The deeply nesting widget is now refactored into a
// stateless const widget. No more needless rebuilding!
const _NonsenseWidget(),
],
);
}
}
class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
}
While it’s a little more code, this is much better.
Now the _NonsenseWidget
is built only once and all of the unnecessary rebuilds are gone.
The parent widget can rebuild itself multiple times, but the _NonsenseWidget
doesn’t care - it’s built once and once only.
(This is only one part of the story and applies to const
StatelessWidgets - see this answer for what is the difference between functions and classes to create widgets? - a StackOverflow answer by Remi Rousselet.)
Splitting widgets into smaller widgets - more complex examples
You might be thinking that the above was a really simple example and it doesn’t represent the complexity of a real app.
And you’d be right.
I recently updated the open source inKino app to follow the advice on this article.
For example, I think this is a good sample of splitting widgets into smaller StatelessWidget
s in a bigger app.
Conclusion
Instead of splitting you build methods into multiple smaller methods, split them into StatelessWidget
s.
This way, you won’t be rebuilding your static widget trees multiple times for nothing but wasted CPU cycles.
When it comes to optimizing performance of Flutter apps, this is probably one of the lowest hanging fruits.
If you really prefer building your widget trees with methods, you might want to take a look at a package called functional_widget by Remi Rousselet. It alleviates the problems that come with building widget trees with methods by using code generation.
14 comments
likes