From 44004441ae0841740b6e83828d0355afa7041714 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 22 Dec 2025 11:46:51 -0800 Subject: [PATCH 1/3] Add declarative user dialog example in Flet Introduces a new example app demonstrating a declarative component dialog in Flet. The dialog asynchronously loads user data from an API and displays it, including error handling and loading state. --- .../apps/declarative/component_dialog.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 sdk/python/examples/apps/declarative/component_dialog.py diff --git a/sdk/python/examples/apps/declarative/component_dialog.py b/sdk/python/examples/apps/declarative/component_dialog.py new file mode 100644 index 000000000..2620f9cbd --- /dev/null +++ b/sdk/python/examples/apps/declarative/component_dialog.py @@ -0,0 +1,81 @@ +import asyncio +from typing import Optional, cast + +import httpx + +import flet as ft + + +# ---------- DIALOG COMPONENT ---------- +@ft.component +def UserDialogContent(): + """Component that loads and displays user data""" + loading, set_loading = ft.use_state(True) + name, set_name = ft.use_state("") + email, set_email = ft.use_state("") + error, set_error = ft.use_state("") + + async def load_user(): + set_loading(True) + set_error("") + try: + await asyncio.sleep(2) # Simulate network delay + async with httpx.AsyncClient(timeout=5) as client: + r = await client.get("https://jsonplaceholder.typicode.com/users/1") + r.raise_for_status() + data = r.json() + set_name(data["name"]) + set_email(data["email"]) + except Exception as e: + set_error(str(e)) + finally: + set_loading(False) + + # Load data when component mounts + ft.use_effect(lambda: asyncio.create_task(load_user()), []) + + return ft.Column( + tight=True, + controls=[ + ft.Text("User Panel", weight=ft.FontWeight.BOLD, size=18), + ft.ProgressRing(visible=loading), + ft.Text(f"Name: {name}"), + ft.Text(f"Email: {email}"), + ft.Text(error, color=ft.Colors.RED) if error else ft.Container(), + ], + ) + + +# ---------- PARENT COMPONENT ---------- +@ft.component +def App(): + dlg_ref = ft.use_ref(cast(Optional[ft.AlertDialog], None)) + + if dlg_ref.current is None: + dlg_ref.current = ft.AlertDialog( + modal=True, + title=ft.Text("User Information"), + content=UserDialogContent(), + actions=[ft.TextButton("Close", on_click=lambda e: e.page.pop_dialog())], + actions_alignment=ft.MainAxisAlignment.END, + ) + + def open_user_dialog(): + if dlg_ref.current: + ft.context.page.show_dialog(dlg_ref.current) + + return ft.Container( + padding=20, + content=ft.Column( + controls=[ + ft.Text("Main App", size=22, weight=ft.FontWeight.BOLD), + ft.ElevatedButton( + "Open User Panel", + on_click=open_user_dialog, + ), + ] + ), + ) + + +ft.run(lambda page: page.render(App)) From e82372b7f8f42b3c2d38390d9005bf26a73f597f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 22 Dec 2025 17:27:42 -0800 Subject: [PATCH 2/3] Add deep linking bootstrap and route info provider Introduces FletDeepLinkingBootstrap to capture and replay deep links before the router is ready, and FletRouteInformationProvider to normalize external URIs for routing. Updates page control to use these for improved deep linking support and initial route handling. --- packages/flet/lib/flet.dart | 1 + packages/flet/lib/src/controls/page.dart | 18 +++++ .../src/routing/deep_linking_bootstrap.dart | 72 +++++++++++++++++++ .../routing/route_information_provider.dart | 29 ++++++++ 4 files changed, 120 insertions(+) create mode 100644 packages/flet/lib/src/routing/deep_linking_bootstrap.dart create mode 100644 packages/flet/lib/src/routing/route_information_provider.dart diff --git a/packages/flet/lib/flet.dart b/packages/flet/lib/flet.dart index 140262af6..06bbf9178 100644 --- a/packages/flet/lib/flet.dart +++ b/packages/flet/lib/flet.dart @@ -15,6 +15,7 @@ export 'src/flet_service.dart'; export 'src/models/asset_source.dart'; export 'src/models/control.dart'; export 'src/models/page_size_view_model.dart'; +export 'src/routing/deep_linking_bootstrap.dart'; export 'src/testing/test_finder.dart'; export 'src/testing/tester.dart'; export 'src/utils.dart'; diff --git a/packages/flet/lib/src/controls/page.dart b/packages/flet/lib/src/controls/page.dart index 0a05eaafc..8b97788bd 100644 --- a/packages/flet/lib/src/controls/page.dart +++ b/packages/flet/lib/src/controls/page.dart @@ -17,6 +17,8 @@ import '../models/control.dart'; import '../models/keyboard_event.dart'; import '../models/multi_view.dart'; import '../models/page_design.dart'; +import '../routing/deep_linking_bootstrap.dart'; +import '../routing/route_information_provider.dart'; import '../routing/route_parser.dart'; import '../routing/route_state.dart'; import '../routing/router_delegate.dart'; @@ -55,6 +57,7 @@ class _PageControlState extends State with WidgetsBindingObserver { late final RouteState _routeState; late final SimpleRouterDelegate _routerDelegate; late final RouteParser _routeParser; + late final RouteInformationProvider _routeInformationProvider; late final AppLifecycleListener _appLifecycleListener; ServiceRegistry? _services; String? _servicesUid; @@ -78,6 +81,19 @@ class _PageControlState extends State with WidgetsBindingObserver { _updateMultiViews(); _routeParser = RouteParser(); + final defaultRouteName = + WidgetsBinding.instance.platformDispatcher.defaultRouteName; + final pendingInitial = + FletDeepLinkingBootstrap.takePendingInitialRouteInformation(); + final initial = FletRouteInformationProvider.normalize( + pendingInitial ?? + RouteInformation( + uri: Uri.tryParse(defaultRouteName) ?? Uri(path: '/'), + ), + ); + _routeInformationProvider = + FletRouteInformationProvider(initialRouteInformation: initial); + FletDeepLinkingBootstrap.markRouterReady(); _routeState = RouteState(_routeParser); _routeState.addListener(_routeChanged); @@ -528,6 +544,7 @@ class _PageControlState extends State with WidgetsBindingObserver { showSemanticsDebugger: showSemanticsDebugger, routerDelegate: _routerDelegate, routeInformationParser: _routeParser, + routeInformationProvider: _routeInformationProvider, title: windowTitle, theme: cupertinoTheme, builder: scaffoldMessengerBuilder, @@ -553,6 +570,7 @@ class _PageControlState extends State with WidgetsBindingObserver { showSemanticsDebugger: showSemanticsDebugger, routerDelegate: _routerDelegate, routeInformationParser: _routeParser, + routeInformationProvider: _routeInformationProvider, title: windowTitle, theme: lightTheme, darkTheme: darkTheme, diff --git a/packages/flet/lib/src/routing/deep_linking_bootstrap.dart b/packages/flet/lib/src/routing/deep_linking_bootstrap.dart new file mode 100644 index 000000000..2aea82a82 --- /dev/null +++ b/packages/flet/lib/src/routing/deep_linking_bootstrap.dart @@ -0,0 +1,72 @@ +import 'package:flutter/widgets.dart'; + +/// Captures cold-start deep links delivered before Flet's `*.router` app is +/// mounted, and replays them as the initial route once the router is ready. +/// +/// Usage in a host app (before `runApp()`): +/// `FletDeepLinkingBootstrap.install();` +class FletDeepLinkingBootstrap { + static final _observer = _FletDeepLinkObserver(); + static bool _installed = false; + + static RouteInformation? _pendingInitialRouteInformation; + + /// Installs a `WidgetsBindingObserver` as early as possible (ideally right + /// after `WidgetsFlutterBinding.ensureInitialized()`). + static void install() { + if (_installed) return; + WidgetsFlutterBinding.ensureInitialized(); + WidgetsBinding.instance.addObserver(_observer); + _installed = true; + } + + /// Called by Flet once its Router (`MaterialApp.router`/`CupertinoApp.router`) + /// is ready to receive route updates. + static void markRouterReady() { + if (!_installed) return; + WidgetsBinding.instance.removeObserver(_observer); + _installed = false; + } + + static RouteInformation? takePendingInitialRouteInformation() { + final value = _pendingInitialRouteInformation; + _pendingInitialRouteInformation = null; + return value; + } + + static bool _capture(RouteInformation routeInformation) { + // Only capture the first pending route to avoid swallowing later deep links + // that the host app might want to handle while Flet isn't mounted yet. + if (_pendingInitialRouteInformation != null) { + return false; + } + + final uri = routeInformation.uri; + if (uri.toString().isEmpty) { + return false; + } + + _pendingInitialRouteInformation = routeInformation; + return true; + } +} + +class _FletDeepLinkObserver with WidgetsBindingObserver { + @override + Future didPushRouteInformation( + RouteInformation routeInformation, + ) async { + // Returning true prevents iOS from logging: + // "Failed to handle route information in Flutter." + return FletDeepLinkingBootstrap._capture(routeInformation); + } + + @override + Future didPushRoute(String route) async { + final uri = Uri.tryParse(route); + if (uri == null) { + return false; + } + return FletDeepLinkingBootstrap._capture(RouteInformation(uri: uri)); + } +} diff --git a/packages/flet/lib/src/routing/route_information_provider.dart b/packages/flet/lib/src/routing/route_information_provider.dart new file mode 100644 index 000000000..06b9f74c9 --- /dev/null +++ b/packages/flet/lib/src/routing/route_information_provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter/widgets.dart'; + +/// Normalizes external URIs (e.g. `flet://flet-host/aaa`) into the path-based +/// form used by Flet routing (e.g. `/aaa`). +class FletRouteInformationProvider extends PlatformRouteInformationProvider { + FletRouteInformationProvider({ + required super.initialRouteInformation, + }); + + static RouteInformation normalize(RouteInformation routeInformation) { + final uri = routeInformation.uri; + return RouteInformation( + uri: Uri( + path: uri.path.isEmpty ? '/' : uri.path, + query: uri.query, + fragment: uri.fragment, + ), + state: routeInformation.state, + ); + } + + @override + Future didPushRouteInformation(RouteInformation routeInformation) { + final normalized = normalize(routeInformation); + debugPrint( + "FletRouteInformationProvider.didPushRouteInformation: ${routeInformation.uri} -> ${normalized.uri}"); + return super.didPushRouteInformation(normalized); + } +} From 7032d8488877044c82b0d63a7c697eccc14181d7 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 22 Dec 2025 17:38:26 -0800 Subject: [PATCH 3/3] Fix RouteInformation to omit empty query and fragment Updated FletRouteInformationProvider to only include query and fragment in the URI if they are present, preventing empty values from being set. --- packages/flet/lib/src/routing/route_information_provider.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flet/lib/src/routing/route_information_provider.dart b/packages/flet/lib/src/routing/route_information_provider.dart index 06b9f74c9..2944d40e0 100644 --- a/packages/flet/lib/src/routing/route_information_provider.dart +++ b/packages/flet/lib/src/routing/route_information_provider.dart @@ -12,8 +12,8 @@ class FletRouteInformationProvider extends PlatformRouteInformationProvider { return RouteInformation( uri: Uri( path: uri.path.isEmpty ? '/' : uri.path, - query: uri.query, - fragment: uri.fragment, + query: uri.hasQuery ? uri.query : null, + fragment: uri.hasFragment ? uri.fragment : null, ), state: routeInformation.state, );