Skip to content

Conversation

@ndonkoHenri
Copy link
Contributor

@ndonkoHenri ndonkoHenri commented Nov 21, 2025

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.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've reviewed this pull request using the Sourcery rules engine

@cloudflare-workers-and-pages
Copy link

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

Copilot finished reviewing on behalf of ndonkoHenri November 21, 2025 11:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a post_frame_callback mechanism to solve timing issues with implicit animations that need to wait for the first frame to render before state changes can be animated. It also fixes a critical bug in the container control's animation event triggering.

  • Adds Page.post_frame_callback() method for scheduling callbacks after the first frame renders
  • Implements on_first_frame lifecycle event in both Python and Dart
  • Fixes missing comma in container.dart's triggerEvent call that would have caused a syntax error

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
sdk/python/packages/flet/src/flet/controls/page.py Adds on_first_frame event property and implements post_frame_callback() with callback queuing and execution logic
packages/flet/lib/src/controls/page.dart Implements _scheduleFirstFrameNotification() to emit first_frame event using Flutter's addPostFrameCallback
packages/flet/lib/src/controls/container.dart Fixes syntax error in triggerEvent call by adding missing comma between arguments

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +562 to +576
if self.__first_frame_fired:
self.__run_first_frame_callback(callback)
else:
self.__first_frame_callbacks.append(callback)

def __handle_first_frame_event(self):
"""Drain and execute callbacks when the Flutter client signals first frame."""
if self.__first_frame_fired:
return

self.__first_frame_fired = True
callbacks = self.__first_frame_callbacks[:]
self.__first_frame_callbacks.clear()
for cb in callbacks:
self.__run_first_frame_callback(cb)
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition: The __first_frame_callbacks list and __first_frame_fired flag are accessed without synchronization. If post_frame_callback() is called from a different thread while __handle_first_frame_event() is executing, the callback could be:

  1. Added to the list after it's been copied but before it's cleared (line 573-574), causing it to be lost
  2. Checked against __first_frame_fired between lines 569-572, seeing False, then the flag gets set to True before appending (line 565), causing the callback to never execute

Consider using a lock (e.g., self._lock if available, or a new dedicated lock) to protect access to both __first_frame_fired and __first_frame_callbacks.

Copilot uses AI. Check for mistakes.
if inspect.isawaitable(result):
await result
except Exception:
logger.exception("Error running post_frame_callback callback")
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message in the exception handler is redundant and unclear. It says "Error running post_frame_callback callback" where "post_frame_callback" and "callback" are repeated. Consider simplifying to "Error running post-frame callback" or "Error in post_frame_callback".

Suggested change
logger.exception("Error running post_frame_callback callback")
logger.exception("Error running post-frame callback")

Copilot uses AI. Check for mistakes.
@ndonkoHenri ndonkoHenri changed the title Post frame callback feat: Page.post_frame_callback Nov 21, 2025
@FeodorFitsner
Copy link
Contributor

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v1] Shimmer effect on ft.Container using on_animation_end

3 participants