From ac0ab7256c7551ab82120079f20cd4aad8af594a Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 20 Nov 2025 16:17:39 +0100 Subject: [PATCH 1/3] Fix syntax error in animation end event trigger --- packages/flet/lib/src/controls/container.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flet/lib/src/controls/container.dart b/packages/flet/lib/src/controls/container.dart index 124564cfea..1684977e49 100644 --- a/packages/flet/lib/src/controls/container.dart +++ b/packages/flet/lib/src/controls/container.dart @@ -50,7 +50,7 @@ class ContainerControl extends StatelessWidget with FletStoreMixin { Widget? container; var onAnimationEnd = control.getBool("on_animation_end", false)! - ? () => control.triggerEvent("animation_end" "container") + ? () => control.triggerEvent("animation_end", "container") : null; if ((onClick || url != null || onLongPress || onHover || onTapDown) && ink && From 66e44e3b73475b52939e2f321fece8fa8c515204 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 20 Nov 2025 16:23:59 +0100 Subject: [PATCH 2/3] Add support for first frame lifecycle event callbacks --- packages/flet/lib/src/controls/page.dart | 25 ++++++-- .../packages/flet/src/flet/controls/page.py | 60 +++++++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/packages/flet/lib/src/controls/page.dart b/packages/flet/lib/src/controls/page.dart index 7edb16519f..683561e2bf 100644 --- a/packages/flet/lib/src/controls/page.dart +++ b/packages/flet/lib/src/controls/page.dart @@ -61,6 +61,9 @@ class _PageControlState extends State with WidgetsBindingObserver { bool _keyboardHandlerSubscribed = false; String? _prevViewRoutes; + // Ensures that the first_frame lifecycle event is fired only once. + bool _firstFrameEventSent = false; + final Map _multiViews = {}; bool _registeredFromMultiViews = false; List? _appliedDeviceOrientations; @@ -72,6 +75,7 @@ class _PageControlState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); _updateMultiViews(); + _scheduleFirstFrameNotification(); _routeParser = RouteParser(); @@ -252,6 +256,18 @@ class _PageControlState extends State with WidgetsBindingObserver { } } + /// Schedules a one-time callback to emit the `first_frame` lifecycle event + /// after the first Flutter frame is rendered for this page. + void _scheduleFirstFrameNotification() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _firstFrameEventSent) { + return; + } + _firstFrameEventSent = true; + widget.control.triggerEventWithoutSubscribers("first_frame"); + }); + } + void _routeChanged() { FletBackend.of(context).onRouteUpdated(_routeState.route); } @@ -341,8 +357,7 @@ class _PageControlState extends State with WidgetsBindingObserver { var appStatus = context .select((backend) => (isLoading: backend.isLoading, error: backend.error)); - var appStartupScreenMessage = - backend.appStartupScreenMessage ?? ""; + var appStartupScreenMessage = backend.appStartupScreenMessage ?? ""; var formattedErrorMessage = backend.formatAppErrorMessage(appStatus.error); @@ -503,14 +518,12 @@ class _PageControlState extends State with WidgetsBindingObserver { var backend = FletBackend.of(context); var showAppStartupScreen = backend.showAppStartupScreen ?? false; - var appStartupScreenMessage = - backend.appStartupScreenMessage ?? ""; + var appStartupScreenMessage = backend.appStartupScreenMessage ?? ""; var appStatus = context.select( (backend) => (isLoading: backend.isLoading, error: backend.error)); - var formattedErrorMessage = - backend.formatAppErrorMessage(appStatus.error); + var formattedErrorMessage = backend.formatAppErrorMessage(appStatus.error); var views = widget.control.children("views"); List> pages = []; diff --git a/sdk/python/packages/flet/src/flet/controls/page.py b/sdk/python/packages/flet/src/flet/controls/page.py index 2958ac5a70..40b393be8e 100644 --- a/sdk/python/packages/flet/src/flet/controls/page.py +++ b/sdk/python/packages/flet/src/flet/controls/page.py @@ -1,4 +1,5 @@ import asyncio +import inspect import logging import sys import threading @@ -424,6 +425,16 @@ class Page(BasePage): """ TBD """ + on_first_frame: Optional[ControlEventHandler["Page"]] = None + """ + Called once after the client renders the very first frame. + + Useful for starting implicit animations or other work that must wait until + the initial layout completes. + + Pair with [`page.after_first_frame()`](#after_first_frame) to register + callbacks without wiring up an explicit event handler. + """ _services: list[Service] = field(default_factory=list) _user_services: ServiceRegistry = field(default_factory=lambda: ServiceRegistry()) @@ -447,6 +458,8 @@ def __post_init__( self.__last_route = None self.__query: QueryString = QueryString(self) self.__authorization: Optional[Authorization] = None + self.__first_frame_callbacks: list[Callable[[], Any]] = [] + self.__first_frame_fired = False def get_control(self, id: int) -> Optional[BaseControl]: """ @@ -523,8 +536,55 @@ def before_event(self, e: ControlEvent): if view_index is not None: e.view = views[view_index] + elif e.name == "first_frame": + self.__handle_first_frame_event() + return super().before_event(e) + def after_first_frame(self, callback: Callable[[], Any]): + """ + Schedule a callable to run right after the client renders its first frame. + + Args: + callback: A synchronous function or coroutine function to execute after the + initial layout completes. Already-rendered pages trigger the callback + immediately. + + Raises: + TypeError: If `callback` is not callable. + """ + if not callable(callback): + raise TypeError("callback must be callable") + + 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) + + def __run_first_frame_callback(self, callback: Callable[[], Any]): + """Execute a queued callback asynchronously, awaiting it if needed.""" + + async def _runner(): + try: + result = callback() + if inspect.isawaitable(result): + await result + except Exception: + logger.exception("Error running after_first_frame callback") + + self.run_task(_runner) + def run_task( self, handler: Callable[InputT, Awaitable[RetT]], From e56d7e1002b7bebe8df79569eafe62448458f212 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 21 Nov 2025 11:49:23 +0100 Subject: [PATCH 3/3] Rename after_first_frame to post_frame_callback --- sdk/python/packages/flet/src/flet/controls/page.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/controls/page.py b/sdk/python/packages/flet/src/flet/controls/page.py index 40b393be8e..96f395296b 100644 --- a/sdk/python/packages/flet/src/flet/controls/page.py +++ b/sdk/python/packages/flet/src/flet/controls/page.py @@ -425,6 +425,7 @@ class Page(BasePage): """ TBD """ + on_first_frame: Optional[ControlEventHandler["Page"]] = None """ Called once after the client renders the very first frame. @@ -432,9 +433,10 @@ class Page(BasePage): Useful for starting implicit animations or other work that must wait until the initial layout completes. - Pair with [`page.after_first_frame()`](#after_first_frame) to register + Pair with [`Page.post_frame_callback()`][flet.Page.post_frame_callback] to register callbacks without wiring up an explicit event handler. """ + _services: list[Service] = field(default_factory=list) _user_services: ServiceRegistry = field(default_factory=lambda: ServiceRegistry()) @@ -541,9 +543,10 @@ def before_event(self, e: ControlEvent): return super().before_event(e) - def after_first_frame(self, callback: Callable[[], Any]): + def post_frame_callback(self, callback: Callable[[], Any]): """ - Schedule a callable to run right after the client renders its first frame. + Schedule a callable to run immediately after the page finishes + rendering its very first frame. Args: callback: A synchronous function or coroutine function to execute after the @@ -581,7 +584,7 @@ async def _runner(): if inspect.isawaitable(result): await result except Exception: - logger.exception("Error running after_first_frame callback") + logger.exception("Error running post_frame_callback callback") self.run_task(_runner)