Flutter Performance Tip: Mind your build

Flutter Performance Tip: Mind your build

Improve your Flutter app's performance by using the build method correctly.

ยท

4 min read

We're developers. We like fast cars and fast Flutter.

Welcome to the first of many Flutter performance tips.

This article also has a companion video that gives the exact same information, but with music and visuals.

First, an overview of widgets!

Immutable-UI-Blueprints.gif

Widgets are immutable UI blueprints for RenderObjects. They're meant to be recreated a lot. For Flutter to remain fast your widgets need to be created fast.

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.

First, 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.

Seconds, you're calling namesThatStartWith from the widget's build method; this is the main issue that we're focussing 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. Also, 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:

demo_screen.jpg

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.

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 to 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 ๐Ÿ‘๐Ÿผ. Point is to keep your builds clean and don't do expensive work directly in the build. Extract that logic to whatever form of state management you use.

This 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);
    }
  }

That's that!

Ask question down below, and let me know ideas for future performance tips. Until then, code fast, Flutter faster. Cheers.

ย