diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c18d329e..35eb75992e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Bug fixes +* Fix a system/edge-swipe back gesture exiting the whole host app instead of navigating back when it lands on an embedded `FletApp` (an app rendered inside another Flet app — e.g. a gallery host running example apps in-process). The embedded app's `WidgetsApp` (`MaterialApp`/`CupertinoApp`) ran the default `NavigationNotification` handler, which reported `SystemNavigator.setFrameworkHandlesBack(false)` for a nested app that couldn't pop (typically a single-view example) and swallowed the notification, so the OS finished the whole activity on back and the host never got to report that it could pop. An embedded page now lets that notification bubble to the host (which re-reports `canHandlePop`) and chains a `ChildBackButtonDispatcher` to the host Router, so a system back propagates to the host and pops the view that embeds it by @FeodorFitsner. * Fix `page.window.maximized = True` intermittently reverting to unmaximized right after startup on macOS, when set in the same patch as `page.title` (e.g. `page.title = "My App"; page.window.maximized = True` in `main()`) by @davidlawson. * Fix `flet build` picking a non-decodable icon/splash image when several files share a base name, producing a machine-dependent `NoDecoderForImageFormatException` from `flutter_launcher_icons`. When an app's `assets` held, say, both `icon.png` and `icon.svg`, `find_platform_image` selected the first match from `glob.glob(...)` — whose order is filesystem-dependent — so the same app could pick `icon.png` on one machine and `icon.svg` on another (SVG is vector and can't be decoded by the raster icon/splash generators), turning a working build into a crash purely based on directory listing order. Candidates are now filtered to formats the generators can actually decode (`.svg` is dropped everywhere; `.icns` stays macOS-only and `.ico` Windows-only) and ranked so a raster image (`.png` first) always wins, making the choice deterministic across machines. When the only supplied image is an SVG (no raster sibling), it's skipped with a build-log warning and the default Flet icon is used instead of crashing by @FeodorFitsner. * Fix modal controls (`AlertDialog`, `CupertinoAlertDialog`, `BottomSheet`, `CupertinoBottomSheet`) crashing to a black screen with "setState()/markNeedsBuild() called during build" when they close in the same frame that another route or overlay opens — e.g. dismissing a bottom sheet and showing a `SnackBar` from one handler. The close path popped the route synchronously during `build`, so the exit animation notified a listener that was mid-build. Each modal now tracks its own `ModalRoute` and closes it in a post-frame callback, popping *that* route (never the topmost one); `View`'s confirm-pop pops its own route too, so a modal dismissed in the same tick as a view pop can no longer dismiss the wrong one by @FeodorFitsner. diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 2caf14dc95..0c99455cfd 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.86.3 +* Fix a system/edge-swipe back gesture exiting the whole host app instead of navigating back when it lands on an embedded `FletApp` (an app rendered inside another Flet app — e.g. a gallery host running example apps in-process). The embedded app's `WidgetsApp` (`MaterialApp`/`CupertinoApp`) ran the default `NavigationNotification` handler, which reported `SystemNavigator.setFrameworkHandlesBack(false)` for a nested app that couldn't pop (typically a single-view example) and swallowed the notification, so the OS finished the whole activity on back and the host never got to report that it could pop. An embedded page now lets that notification bubble to the host (which re-reports `canHandlePop`) and chains a `ChildBackButtonDispatcher` to the host Router, so a system back propagates to the host and pops the view that embeds it. * Fix modal controls (`AlertDialog`, `CupertinoAlertDialog`, `BottomSheet`, `CupertinoBottomSheet`) throwing "setState()/markNeedsBuild() called during build" and blanking the screen when closed in the same frame another route/overlay opens (e.g. a `SnackBar`). Each modal now tracks its own `ModalRoute` and closes it via a post-frame `closeModalRoute()` that pops that specific route; `View`'s confirm-pop pops its own route too, removing the wrong-route race between a dismissing modal and a view pop. ## 0.86.2 diff --git a/packages/flet/lib/src/controls/page.dart b/packages/flet/lib/src/controls/page.dart index 00addab21f..98129bc9ae 100644 --- a/packages/flet/lib/src/controls/page.dart +++ b/packages/flet/lib/src/controls/page.dart @@ -74,6 +74,19 @@ class _PageControlState extends State with WidgetsBindingObserver { final Map _multiViews = {}; bool _registeredFromMultiViews = false; + // True when this page is a nested/embedded app (hosted inside another Flet + // app via FletApp, so its backend has a controlId). Embedded pages chain + // their system back handling to the host through [_childBackButtonDispatcher]. + bool _isEmbedded = false; + + // For an embedded page, a child of the host Router's back button dispatcher. + // Without it, MaterialApp.router/CupertinoApp.router would install their own + // RootBackButtonDispatcher, and a system/edge-swipe back that the embedded + // app can't handle (e.g. it has a single view) would call SystemNavigator.pop() + // and exit the whole host app instead of propagating to the host, which would + // pop the view embedding this app. + ChildBackButtonDispatcher? _childBackButtonDispatcher; + @override void initState() { debugPrint("Page.initState: ${widget.control.id}"); @@ -84,7 +97,8 @@ class _PageControlState extends State with WidgetsBindingObserver { _routeParser = RouteParser(); final backend = FletBackend.of(context); - final isEmbedded = backend.controlId != null; + _isEmbedded = backend.controlId != null; + final isEmbedded = _isEmbedded; final defaultRouteName = WidgetsBinding.instance.platformDispatcher.defaultRouteName; final pendingInitial = isEmbedded @@ -136,6 +150,18 @@ class _PageControlState extends State with WidgetsBindingObserver { super.didChangeDependencies(); _ensureServiceRegistries(); _loadFontsIfNeeded(FletBackend.of(context)); + + // When embedded inside another Flet app, hook this page's system back + // handling into the host Router so an unhandled back propagates to the host + // (which pops the embedding view) instead of exiting the whole app. + if (_isEmbedded && _childBackButtonDispatcher == null) { + final parentBackButtonDispatcher = + Router.maybeOf(context)?.backButtonDispatcher; + if (parentBackButtonDispatcher != null) { + _childBackButtonDispatcher = + parentBackButtonDispatcher.createChildBackButtonDispatcher(); + } + } } @override @@ -583,11 +609,26 @@ class _PageControlState extends State with WidgetsBindingObserver { var showSemanticsDebugger = control.getBool("show_semantics_debugger", false)!; + // Claim back-button priority for the embedded page so its Router (and, on a + // fall-through, the host Router) receives system back events. Re-asserted on + // every build, matching Flutter's nested-Router guidance. + _childBackButtonDispatcher?.takePriority(); + + // For an embedded page, do NOT let WidgetsApp's default NavigationNotification + // handler run: it would call SystemNavigator.setFrameworkHandlesBack(false) + // (this nested app's single view can't pop) and stop the notification, so the + // OS finishes the whole activity on a system/edge-swipe back. Returning false + // lets the notification bubble to the host app, whose Navigator re-reports + // canHandlePop=true when the host can pop the view embedding this app. + final NotificationListenerCallback? + onNavigationNotification = _isEmbedded ? (_) => false : null; + Widget? app = widgetsDesign == PageDesign.cupertino ? home != null ? CupertinoApp( debugShowCheckedModeBanner: false, showSemanticsDebugger: showSemanticsDebugger, + onNavigationNotification: onNavigationNotification, title: windowTitle, theme: cupertinoTheme, builder: scaffoldMessengerBuilder, @@ -599,9 +640,11 @@ class _PageControlState extends State with WidgetsBindingObserver { : CupertinoApp.router( debugShowCheckedModeBanner: false, showSemanticsDebugger: showSemanticsDebugger, + onNavigationNotification: onNavigationNotification, routerDelegate: _routerDelegate, routeInformationParser: _routeParser, routeInformationProvider: _routeInformationProvider, + backButtonDispatcher: _childBackButtonDispatcher, title: windowTitle, theme: cupertinoTheme, builder: scaffoldMessengerBuilder, @@ -613,6 +656,7 @@ class _PageControlState extends State with WidgetsBindingObserver { ? MaterialApp( debugShowCheckedModeBanner: false, showSemanticsDebugger: showSemanticsDebugger, + onNavigationNotification: onNavigationNotification, title: windowTitle, theme: lightTheme, darkTheme: darkTheme, @@ -626,9 +670,11 @@ class _PageControlState extends State with WidgetsBindingObserver { : MaterialApp.router( debugShowCheckedModeBanner: false, showSemanticsDebugger: showSemanticsDebugger, + onNavigationNotification: onNavigationNotification, routerDelegate: _routerDelegate, routeInformationParser: _routeParser, routeInformationProvider: _routeInformationProvider, + backButtonDispatcher: _childBackButtonDispatcher, title: windowTitle, theme: lightTheme, darkTheme: darkTheme,