Flutter Performance Tip: Keep Your Build Method Pure

Improve your Flutter app's performance by keeping the build method pure and free of side effects.

Flutter Performance Tip: Keep Your Build Method Pure
ON THIS PAGE

Understand what build cost is and how to keep your builds pure and free of side effects.

What are Widgets?

💡
Widgets are immutable UI blueprints for RenderObjects. Widgets are meant to be recreated a lot and the RenderObjects are meant to be reused as often as possible.

Keep this in mind as you read this article: for Flutter to remain blazingly fast your widgets need to be created fast.

Widget Blueprints and associated Render Objects

Problem Code

Lets explore an example. Keep in mind it's just an example to illustrate a point.

...

/// Not really long, just a demo.
const superLongListOfNames = ['Gordon', 'George', 'Hannah', 'Harry', 'Dan'];

/// Expensive operation, just a demo.
List<String> namesThatStartWith(String l) {
  print('expensive function $l');
  return superLongListOfNames
      .where((element) => element.startsWith(l))
      .toList();
}

class OhNoWidget extends StatelessWidget {
  const OhNoWidget({
    Key? key,
    required this.letter,
  }) : super(key: key);

  final String letter;

  @override
  Widget build(BuildContext context) {
    print('building');
    List<String> filteredNames = namesThatStartWith(letter); // This is the ouchie

    return ListView.builder(
      itemCount: filteredNames.length,
      itemBuilder: (context, index) {
        return Padding(
          padding: const EdgeInsets.all(32.0),
          child: _Tile(title: filteredNames[index]),
        );
      },
    );
  }
}

...

Above you have a StatelessWidget; this widget takes in a String property, called letter.

You use this property to filter a long list of names that start with that letter. You have a function called namesThatStartWith and you filter the list superLongListOfNames.

You call this function in the build method, and then you use the result to display the names that start with that letter.

There are two problems with this widget.

  1. If you assume this list is really long, then you should probably do this operation in a separate isolate instead. We'll talk about isolates in a future article.
  2. We're calling namesThatStartWith from the widget's build method; this is the main issue that we'll focus on in this article.

This means this operation will be performed every time the build method is called. Meaning any time a parent is updated, or new (unrelated) state is passed in, or the user navigates the app. The build method is supposed to be called a lot, and that is part of how Flutter is designed. Keep in mind that you don't always have control over when a build is triggered.

Here is an example of what this output would look like:

Emulator output of example code

There's an extra button to trigger a call to setState higher up in the widget tree.

When that button is tapped the build method will be called again and as a side effect the expensive function will be called as well.

Solution

Keep your build method pure and free of side effects!

In this example you can fix the issue by making this a StatefulWidget, and moving the call to the expensive function inside initState.

class OhNoWidget extends StatefulWidget {
  const OhNoWidget({
    Key? key,
    required this.letter,
  }) : super(key: key);

  final String letter;

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

class _OhNoWidgetState extends State<OhNoWidget> {
  late List<String> filteredNames;

  @override
  void initState() {
    super.initState();
    filteredNames = namesThatStartWith(widget.letter); // Moved here
  }

  @override
  Widget build(BuildContext context) {
    print('building');

    return ListView.builder(
      itemCount: filteredNames.length,
      itemBuilder: (context, index) {
        return Padding(
          padding: const EdgeInsets.all(32.0),
          child: _Tile(title: filteredNames[index]),
        );
      },
    );
  }
}

And there you go 👍🏼. Now this will only be called the first time the widget is created, and not for each build.

Point is to keep your builds clean and not to perform expensive work directly in the build. Extract that logic to whatever form of state management you use.

Handling State Updates

The logic would look different depending on how you manage your state and trigger rebuilds in your app.

For this particular example when using setState, take note that if the letter property you pass in were to change, then you'd need to filter the list again.

You can do that by overriding didUpdateWidget.

Just add a check to see if the oldWidget.letter value and the new widget.letter value are different. If yes, recompute and set the local filteredNames state.

@override
  void didUpdateWidget(OhNoWidget oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (oldWidget.letter != widget.letter) {
      filteredNames = namesThatStartWith(widget.letter);
    }
  }

The main point is that you only want to be recomputing when necessary.

That's that!

Code fast, Flutter faster.