Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/flet/lib/flet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
18 changes: 18 additions & 0 deletions packages/flet/lib/src/controls/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -55,6 +57,7 @@ class _PageControlState extends State<PageControl> 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;
Expand All @@ -78,6 +81,19 @@ class _PageControlState extends State<PageControl> 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);
Expand Down Expand Up @@ -528,6 +544,7 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
showSemanticsDebugger: showSemanticsDebugger,
routerDelegate: _routerDelegate,
routeInformationParser: _routeParser,
routeInformationProvider: _routeInformationProvider,
title: windowTitle,
theme: cupertinoTheme,
builder: scaffoldMessengerBuilder,
Expand All @@ -553,6 +570,7 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
showSemanticsDebugger: showSemanticsDebugger,
routerDelegate: _routerDelegate,
routeInformationParser: _routeParser,
routeInformationProvider: _routeInformationProvider,
title: windowTitle,
theme: lightTheme,
darkTheme: darkTheme,
Expand Down
72 changes: 72 additions & 0 deletions packages/flet/lib/src/routing/deep_linking_bootstrap.dart
Original file line number Diff line number Diff line change
@@ -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<bool> didPushRouteInformation(
RouteInformation routeInformation,
) async {
// Returning true prevents iOS from logging:
// "Failed to handle route information in Flutter."
return FletDeepLinkingBootstrap._capture(routeInformation);
}

@override
Future<bool> didPushRoute(String route) async {
final uri = Uri.tryParse(route);
if (uri == null) {
return false;
}
return FletDeepLinkingBootstrap._capture(RouteInformation(uri: uri));
}
}
29 changes: 29 additions & 0 deletions packages/flet/lib/src/routing/route_information_provider.dart
Original file line number Diff line number Diff line change
@@ -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<bool> didPushRouteInformation(RouteInformation routeInformation) {
final normalized = normalize(routeInformation);
debugPrint(
"FletRouteInformationProvider.didPushRouteInformation: ${routeInformation.uri} -> ${normalized.uri}");
return super.didPushRouteInformation(normalized);
}
}
81 changes: 81 additions & 0 deletions sdk/python/examples/apps/declarative/component_dialog.py
Original file line number Diff line number Diff line change
@@ -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))
Loading