flet icon indicating copy to clipboard operation
flet copied to clipboard

feat: `Page.post_frame_callback`

Open ndonkoHenri opened this issue 1 month ago • 2 comments

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.

ndonkoHenri avatar Nov 21 '25 11:11 ndonkoHenri

Deploying flet-docs with  Cloudflare Pages  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

View logs

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?

FeodorFitsner avatar Nov 25 '25 03:11 FeodorFitsner