Understanding and Avoiding Unexpected Component Rebuilds in Flutter

gguedes

Gustavo Guedes

Posted on August 9, 2024

Understanding and Avoiding Unexpected Component Rebuilds in Flutter

A little context

Recently, I encountered a problem that took me a few hours to understand what was happening. Let's dive straight into the example:

Imagine the following component:

class CustomButton extends StatefulWidget {
  const CustomButton({super.key});

  @override
  State<CustomButton> createState() => _CustomButtonState();
}

class _CustomButtonState extends State<CustomButton> {
  @override
  void initState() {
    print('INIT STATE CUSTOM BUTTON');
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {},
      child: const Text("Do something"),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Simple, right? But this initState gave me quite a headache.

Basically, every time the screen entered or exited the loading state, the initState was triggered. As we know, initState should only be executed once, when the component is first placed on the screen.

So I asked myself: what was destroying and rebuilding this component repeatedly? The answer is simple but interesting. Take a look:

When loading is more than true or false

Think of a complex component where there are multiple levels of loading: from the parent and from the component itself. A good example is a TODO list. It wouldn't be a great experience to disable the entire list just to update a single item on the backend.

We can take an approach like this:

sample-about-loading-states

Here we have the global loading of the list and the individual loading of each item. So where's the problem?

See what happens when we toggle the loading status:

flutter: PAGE initState
flutter: PAGE BUILD
flutter: List Item initState
flutter: List Item BUILD
flutter: PAGE BUILD
flutter: List Item initState
flutter: List Item BUILD
Enter fullscreen mode Exit fullscreen mode

Notice that the initState of the List Item is called twice, while the screen's is called only once. In a list with simple items, this might not seem like a big issue, but when we talk about more complex lists or a screen that contains components imported from other features with their own initState routines, the problem can easily escalate. Can you see where this might lead?

Imagine, for instance, that every time our List Item sends a view event to an Analytics service, and this happens in the component's initState. The data about this widget's views would be considerably inflated because of this "problem."

Are there solutions?

Of course! But the solution depends on the scale of the problem.

In the case above, it would be easier to send the component's view information before the list is displayed, rather than within the component itself. This is one possibility. However, in larger applications, it's not sustainable to manage everything in one place.

The main point is to know exactly where to use your loading state and when to segment it.

I encountered this specific problem when using the shimmer library. The effect I used above was created with it. Every time a component's loading finished, the data in an input was cleared, precisely because the initState of the component was clearing the TextEditingController.

class CustomShimmer extends StatelessWidget {
  final Widget child;
  final bool loading;
  final Color baseColor;
  final Color highlightColor;

  const CustomShimmer({
    super.key,
    required this.child,
    this.loading = false,
    this.baseColor = Colors.black,
    this.highlightColor = Colors.white60,
  });

  @override
  Widget build(BuildContext context) {
    if (loading) {
      return Shimmer.fromColors(
        baseColor: baseColor,
        highlightColor: highlightColor,
        child: child,
      );
    }

    return child;
  }
}
Enter fullscreen mode Exit fullscreen mode

As mentioned, every time the component changes state, our child is destroyed and recreated. It's important to clarify that this is not a problem with the library; if it were a CircularProgressIndicator, the same thing would happen.

Conclusion

The goal of this post was to highlight something simple but that can easily go unnoticed and end up generating an issue on your board in the future. Stay alert to these details! 😊

💖 💪 🙅 🚩
gguedes
Gustavo Guedes

Posted on August 9, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related