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"),
);
}
}
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:
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
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;
}
}
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! 😊
Top comments (1)
Hi Gustavo Guedes,
Top, very nice and helpful!
Thanks for sharing.