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..2944d40e0 --- /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.hasQuery ? uri.query : null, + fragment: uri.hasFragment ? uri.fragment : null, + ), + state: routeInformation.state, + ); + } + + @override + Future didPushRouteInformation(RouteInformation routeInformation) { + final normalized = normalize(routeInformation); + debugPrint( + "FletRouteInformationProvider.didPushRouteInformation: ${routeInformation.uri} -> ${normalized.uri}"); + return super.didPushRouteInformation(normalized); + } +} 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))