From 6bef1e6fb8f0480d3fc82218a9b91af2d476c8f5 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 3 Jul 2026 19:07:17 -0700 Subject: [PATCH 1/2] fix(build): select web URL strategy before boot, not in prepareApp() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build template called usePathUrlStrategy() from prepareApp(), which runs after the boot UI mounts. By then Flutter's web routing had already initialized under the default hash strategy, so path-routed web apps got /#/... URLs. Move the strategy selection to the first statement in main(), before FletDeepLinkingBootstrap.install() (which calls ensureInitialized) and any runApp() — matching the before-boot ordering already used in client/lib/main.dart. Necessary for path routing but not sufficient for cold-start deep links; see the follow-up boot-overlay fix. --- .../{{cookiecutter.out_dir}}/lib/main.dart | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart index fdd77c643d..0ceaf06611 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart @@ -43,6 +43,20 @@ String appDir = ""; Map environmentVariables = Map.from(Platform.environment); void main(List args) async { + // On web the URL strategy must be selected before ANYTHING reads the initial + // location: FletDeepLinkingBootstrap.install() (below) calls + // WidgetsFlutterBinding.ensureInitialized() and starts observing route pushes, + // and the first runApp() wires up the router. Whatever strategy is active at + // that point latches how the cold-start URL maps to a route — under Flutter's + // default hash strategy a hard refresh of `/apps/x` reads an empty fragment + // (route "/"), so the deep link is lost and the app rewrites the URL to "/". + // Applying the strategy that `flet build` baked into window.flet here, as the + // very first statement, keeps both the running URLs (no `/#/`) and cold-start + // deep links on the path strategy. + if (kIsWeb && getFletRouteUrlStrategy() == "path") { + usePathUrlStrategy(); + } + FletDeepLinkingBootstrap.install(); _args = List.from(args); @@ -301,12 +315,9 @@ Future prepareApp() async { ); if (kIsWeb) { - // web mode - connect via HTTP + // web mode - connect via HTTP. The URL strategy is applied in main() before + // runApp(); doing it here would be too late (see the note there). pageUrl = Uri.base.toString(); - var routeUrlStrategy = getFletRouteUrlStrategy(); - if (routeUrlStrategy == "path") { - usePathUrlStrategy(); - } assetsDir = getAssetsDir(); } else if (_args.isNotEmpty && isDesktopPlatform()) { // developer mode From 44476cfc20a23921608237fd0d43807c70520383 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 3 Jul 2026 19:59:42 -0700 Subject: [PATCH 2/2] fix(build): don't let the boot-screen MaterialApp report route '/' to the browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The boot overlay wrapped the boot screen in MaterialApp(home: ...). A MaterialApp with `home` builds its Navigator with reportsRouteUpdateToEngine: true, so on web it pushes the home route '/' to the browser URL the moment it mounts — during boot, before FletApp's real router reads the location. A cold-start deep link like /gallery or /apps/ was therefore overwritten to '/'. Verified with a headless-Chromium probe: pre-fix the URL reset /gallery -> / ~1.7s after 'Flutter app loaded' (before the Python worker even ran); post-fix /gallery holds. Render the boot screen via `builder:` instead of `home:`. With no home/routes/onGenerateRoute, WidgetsApp creates no Navigator (per its own docs) and reports nothing to the engine, so the deep link survives until the app's router takes over. The overlay never navigates, so it needs no Navigator. Regression from the 0.86 boot-screen rework (#6616); 0.85.3 rendered a bare SizedBox during boot, which reported nothing. --- .../build/{{cookiecutter.out_dir}}/lib/main.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart index 0ceaf06611..8378f92626 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart @@ -282,9 +282,16 @@ class _BootOverlayState extends State<_BootOverlay> { // resolveBootScreen is built once here (status changes update the // message via the screen's own ValueListenableBuilder), so the spinner // keeps animating across preparing → starting up. + // Use `builder:` rather than `home:` so this MaterialApp creates NO + // Navigator. A MaterialApp with `home` builds a Navigator with + // `reportsRouteUpdateToEngine: true`, which pushes the home route "/" + // to the browser URL on mount — clobbering a cold-start deep link + // (e.g. `/gallery`) before FletApp's real router reads it. The boot + // overlay never navigates, so it doesn't need a Navigator; this keeps + // theme/Directionality while leaving the URL untouched. child: MaterialApp( debugShowCheckedModeBanner: false, - home: resolveBootScreen( + builder: (context, _) => resolveBootScreen( name: bootScreenName, options: bootScreenOptions, extensions: extensions,