addPostFrameCallback
Flutter’s addPostFrameCallback
Ever wondered to run some logic only for one time when the Flutter UI tree is built, well it can be one of the following things.
- Run Some logic once the frame is rendered for the first time and design UI accordingly to adapt to the design needs.
- Showing Dialog once the tree is rendered. (Well adding this logic in initState is not a good idea because it throws setState() or markNeedsBuilds also initState method has no access to context synchronously but for sure will work if you add some future delay that is going to execute itself once the context is available for the current Widget but let's keep this StateFull widget’s lifecycle for next time.)
- Get the Size and Offset of the widget once it's rendered.
But What is addPostFrameCallback?
The addPostFrameCallback method is a part of the WidgetsBinding class which is inherited from SchedulerBinding in Flutter. It allows developers to register a callback that will be invoked after the current frame finishes rendering. The callback receives a timestamp parameter indicating the time at which it was called.
And Now time for action.
We will Explore two examples here as follows.
- Creating a Dialog to be displayed once the widget tree is rendered and ensuring that setState will not re-render this dialog (A toxic relationship).
- Getting Dimensions of widgets created.
🎯Dialog Display.
Well, here the scenario is Dialog has ended her toxic relationship with Button who liked to hurt her for fun, and after getting frustrated from unnecessary calls from Button she has decided to move with addPostFrameCallback. Let’s see if addPostFrameCallback behaves the same as the button and continues hurting her or will protect her from setState’s rebuild triggered by Button.
Happy Endings They Stayed Forever with each other……
and below is the entire story.
Let’s explore this in more detail.
- Dialog to be displayed
void _showDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) {
return Container(
margin: const EdgeInsets.all(20),
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
Icon(
Icons.heart_broken_rounded,
color: Colors.red,
size: 50,
),
Text(
'''Hi, I'm Dialog, and I have moved on from setState. \n Rebuilds won't hurt me any more 🥳 ''',
style: TextStyle(fontSize: 20),
textAlign: TextAlign.center,
)
],
),
),
);
},
);
}
2. Magic in initState().
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_showDialog(context);
});
}
Entire magic happens in initState where a dev registers for a callback when the frame is rendered and once this callback is triggered this widget has entire information about its own context which further contains the widgets layout information (will look at how to extract this information in the second example). The documentation describes this method as follows.
Schedule a callback for the end of this frame.
Does not request a new frame.
This callback is run during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven’t been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.
The callbacks are executed in the order in which they have been added.
Post-frame callbacks cannot be unregistered. They are called exactly once.
Moving to our second example
🎯Getting Dimensions of widgets created.
One use of addPostFrameCallback can be to Perform calculations or measurements: If you need to calculate or measure certain properties of UI elements, such as their size or position, after the frame has been rendered, you can use addPostFrameCallback to schedule the code execution.
In this example, we will create a widget, once this widget is created will extract information about size and position and display this information followed by creating a similar widget with the same measurement at runtime.
Let’s explore the code.
final GlobalKey textKey = GlobalKey();
Size size = const Size(0, 0);
Offset offset = const Offset(0, 0);
setting up variables to get Size and Offset.
GlobalKey uniquely identifies the widget in the render tree as RenderBox.
we need to attach this key to the widget whose information we need for our use.
/// Adding key to Widget
Text(
key: textKey,
'Flutter is awesome',
style: const TextStyle(fontSize: 20),
),
@override
void initState() {
super.initState();
getDimensions();
}
void getDimensions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
getSizeAndPosition();
});
}
getSizeAndPosition() {
RenderBox logoBox = textKey.currentContext!.findRenderObject() as RenderBox;
size = logoBox.size;
offset = logoBox.localToGlobal(Offset.zero);
setState(() {});
}
in the above block, we register an addPostFrameCallback in initState. Thevoid getDimensions()
is a helper method defined in the code. It uses WidgetsBinding.instance.addPostFrameCallback()
to schedule a callback to be called after the current frame is drawn on the screen. This is necessary because the widget tree needs to be fully laid out and rendered before obtaining the size and position of a widget.
getSizeAndPosition()
is another helper method. It obtains the size and position of a widget using a RenderBox
. The line RenderBox logoBox = textKey.currentContext!.findRenderObject() as RenderBox;
retrieves the RenderBox
associated with a specific widget. It uses the findRenderObject()
method to find the associated RenderBox
object.
Once the RenderBox
is obtained, the code retrieves its size and assigns it to the size
variable. It also retrieves the global offset of the widget relative to the screen using localToGlobal(Offset.zero)
. Finally, the setState()
method is called to update the state of the widget, which triggers a rebuild of the widget and its descendants.
And now UI of this Code can be split into two parts.
one before getSizeAndPosition() is triggered and the other after this method is triggered.
Before Callback is triggered
When the initial frame is drawn it has created the Container with zero height and width. Also, the required data for the Text(‘Flutter is awesome’) widget is not yet extracted.
After Callback is triggered
We can see the gradient container with text which is created from the dimensions of the Text(‘Flutter is awesome’) widget. Also, the Dimensions are updated in the black band.
Also if you are implementing this example the next frame is rendered so fast that you will hardly even notice that the gradient widget never existed before.
Happy Coding.