feat: `Page.post_frame_callback`
Fix #5796
In the below code, when change_bgcolor() runs inside did_mount, the page is still in its very first diff cycle. The color bgcolor flip gets merged into the same initial patch that delivers the widget tree to Flutter, so the client receives only one state (blue) and there’s no previous frame to interpolate from (implicit animations only work after the first frame because they need an “old” value to animate away from). As a result, the Container actually starts with a blue bgcolor, and on_animation_end never fires, and the avatar stays 'frozen'.
Code
Note: Try it on the first commit of this PR, which fixes a bug.
import flet as ft
START_BGCOLOR = ft.Colors.YELLOW
END_BGCOLOR = ft.Colors.BLUE
class Profile(ft.Column):
def __init__(self):
super().__init__()
self.avatar = ft.Container(
shape=ft.BoxShape.CIRCLE,
width=100,
height=100,
bgcolor=START_BGCOLOR,
animate=ft.Animation(duration=ft.Duration(milliseconds=1000)),
on_animation_end=self.change_bgcolor,
)
self.controls = [self.avatar]
def did_mount(self):
super().did_mount()
self.change_bgcolor()
def change_bgcolor(self):
print("change_bgcolor")
if self.avatar.bgcolor == START_BGCOLOR:
self.avatar.bgcolor = END_BGCOLOR
else:
self.avatar.bgcolor = START_BGCOLOR
self.update()
def main(page):
page.add(Profile())
ft.run(main)
A wayaround this, is to set a small time delay/sleep, sufficient enough to allow the first frame of the control to painted first (yellow bgcolor), and only after it, request a color change.
Code
Note: Try it on the first commit of this PR, which fixes a bug.
import asyncio
import flet as ft
START_BGCOLOR = ft.Colors.YELLOW
END_BGCOLOR = ft.Colors.BLUE
class Profile(ft.Column):
def __init__(self):
super().__init__()
self.avatar = ft.Container(
shape=ft.BoxShape.CIRCLE,
width=100,
height=100,
bgcolor=START_BGCOLOR,
animate=ft.Animation(duration=ft.Duration(milliseconds=1000)),
on_animation_end=self.change_bgcolor,
)
self.controls = [self.avatar]
def did_mount(self):
super().did_mount()
async def kick_off():
await asyncio.sleep(0.1) # yield so the first frame can paint
self.change_bgcolor()
self.page.run_task(kick_off)
def change_bgcolor(self):
print("change_bgcolor")
if self.avatar.bgcolor == START_BGCOLOR:
self.avatar.bgcolor = END_BGCOLOR
else:
self.avatar.bgcolor = START_BGCOLOR
self.update()
def main(page):
page.add(Profile())
ft.run(main)
This PR introduces a better way to handle such scenarios, in which one will only have to pass a callback to the Page.post_frame_callback method, and it will run at the best time possible.
Code
import flet as ft
START_BGCOLOR = ft.Colors.YELLOW
END_BGCOLOR = ft.Colors.BLUE
class Profile(ft.Column):
def __init__(self):
super().__init__()
self.avatar = ft.Container(
shape=ft.BoxShape.CIRCLE,
width=100,
height=100,
bgcolor=START_BGCOLOR,
animate=ft.Animation(duration=ft.Duration(milliseconds=1000)),
on_animation_end=self.change_bgcolor,
)
self.controls = [self.avatar]
def did_mount(self):
super().did_mount()
self.page.post_frame_callback(self.change_bgcolor)
def change_bgcolor(self):
print("change_bgcolor")
if self.avatar.bgcolor == START_BGCOLOR:
self.avatar.bgcolor = END_BGCOLOR
else:
self.avatar.bgcolor = START_BGCOLOR
self.update()
def main(page):
page.add(Profile())
ft.run(main)
Summary by Sourcery
Add lifecycle support for running page callbacks after the first rendered frame to better support implicit animations and other post-layout work.
New Features:
- Expose a Page.on_first_frame handler in the Python API and a Page.post_frame_callback method to schedule callbacks after the initial frame is rendered.
- Emit a one-time first_frame lifecycle event from the Flutter PageControl after the first Flutter frame using a post-frame callback hook.
Bug Fixes:
- Correct the Container control animation end event wiring so it triggers the animation_end event on the container correctly instead of using a malformed event name.
Enhancements:
- Ensure post-frame callbacks are safely queued, executed once when the first_frame event fires, and awaited if they are async, with errors logged rather than surfacing to the caller.
Documentation:
- Document the new first-frame lifecycle behavior and how to pair on_first_frame with Page.post_frame_callback in the Page API docstring.
Deploying flet-docs with
Cloudflare Pages
| Latest commit: |
e56d7e1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://75f1353f.flet-docs.pages.dev |
| Branch Preview URL: | https://post-frame-callback.flet-docs.pages.dev |
IMHO, proposed solution feels a bit hacky and very specific to a given case.
Why not just animate explicitly:
import asyncio
import logging
import flet as ft
logging.basicConfig(level=logging.DEBUG)
START_BGCOLOR = ft.Colors.YELLOW
END_BGCOLOR = ft.Colors.BLUE
class Profile(ft.Column):
def __init__(self):
super().__init__()
self.avatar = ft.Container(
shape=ft.BoxShape.CIRCLE,
width=100,
height=100,
bgcolor=START_BGCOLOR,
animate=ft.Animation(duration=ft.Duration(milliseconds=1000)),
# on_animation_end=self.change_bgcolor,
)
self.controls = [self.avatar]
def did_mount(self):
super().did_mount()
async def shimmer():
while True:
await asyncio.sleep(1) # yield so the first frame can paint
self.change_bgcolor()
self.page.run_task(shimmer)
def change_bgcolor(self):
print("change_bgcolor")
if self.avatar.bgcolor == START_BGCOLOR:
self.avatar.bgcolor = END_BGCOLOR
else:
self.avatar.bgcolor = START_BGCOLOR
self.update()
def main(page):
page.add(Profile())
ft.run(main)
or use just added Shimmer control?