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
2 changes: 1 addition & 1 deletion .fvmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"flutter": "3.44.7"
"flutter": "3.44.8"
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

### Improvements

* An embedded `FletApp` can now run over the in-process `dart_bridge` transport instead of a socket. Set `url="dartbridge://"` and the client allocates a native channel, delivers its port through the new `FletApp.on_connect` event, and the host serves that port with a `FletDartBridgeServer` — so a Flet program hosted inside another Flet app (a gallery, a preview) exchanges messages at memcpy speed with no socket file, no TCP port, and no `AF_UNIX` path-length limit (which broke embedded apps on the iOS simulator, where the container path overflows `sun_path`). High-throughput `DataChannel`s used by embedded apps (`RawImage`, `MatplotlibChart`) get their own dedicated bridge too. The transport is opt-in and falls back to the existing URL-scheme channels: on web and desktop dev builds, where `dart_bridge` is unavailable, hosts keep using a socket URL by @FeodorFitsner.

* `flet run` can now pass custom arguments to your app script: everything after a `--` separator is forwarded to the script instead of being parsed by Flet, and arrives there as `sys.argv[1:]` - e.g. `flet run --web main.py -- --dataset big.csv --verbose`. Previously there was no way to do this: the app was always launched as `python -u <script>` with no extra arguments, so a flag meant for the app was consumed by the CLI's own parser and rejected with `flet: error: unrecognized arguments: --verbose`. The arguments are re-applied on every hot reload and work in all run modes (desktop, `--web`, `--ios`, `--android`, and `-m` module invocations). Arguments that don't look like options can be passed without the separator (`flet run main.py big.csv`), and mistyped Flet options are still reported as errors - now with a hint to use `--` when they were meant for the app. See [Passing arguments to your app](https://flet.dev/docs/getting-started/running-app#passing-arguments-to-your-app) by @FeodorFitsner.

### Bug fixes

* Fix iOS apps built with `flet build ipa` crashing at startup with `Failed to lookup symbol 'serious_python_run': dlsym(RTLD_DEFAULT, serious_python_run): symbol not found`. `serious_python` shipped `dart_bridge` — which provides the in-process Dart↔Python transport — as a **static** library linked into the app executable. An iOS executable exports nothing to the dynamic symbol table by default and the release build strips local symbols, so the `dlsym` lookups that Dart (`DynamicLibrary.process()`) and Python (`import dart_bridge`) perform at runtime could not resolve. Only release/archive (device) builds under the Swift Package Manager path were affected — debug and simulator builds don't dead-strip, so the failure did not reproduce there, and Android was never affected (its `dart_bridge` is a dynamic `.so`, which exports its symbols). Bumps `serious_python` to **4.4.0**, which ships `dart_bridge` as a dynamic framework — embedded and signed into the app like `Python.xcframework`, with its symbols exported — and re-pins the bundled python-build snapshot to **20260726** (`dart_bridge` **1.5.1 → 1.6.1**, Pyodide 3.14 **314.0.2 → 314.0.3**); the bundled Python versions (**3.12.13 / 3.13.14 / 3.14.6**) are unchanged by @FeodorFitsner.

* Fix `MatplotlibChart` freezing permanently when its platform view is disposed with a frame in flight — the common trigger is switching to another tab inside the app, which races the frame stream: `DataChannel.send` on a disposed channel silently drops, so the frame's `[0xFF]` frame-applied ack never arrives and `_send_and_wait`'s unbounded await parks `MatplotlibChart._receive_loop` — the sole consumer of the frame queue — for the rest of the session, with no exception raised; remounting opens a fresh channel but the stale ack futures were never resolved, so the chart stayed frozen. `_capture_channel` now resolves all pending ack futures when a new channel is captured (a fresh channel means every pending ack belongs to the disposed one), unparking the producer instantly on remount, and the ack await is bounded by `FRAME_ACK_TIMEOUT` (5s; a healthy ack lands in milliseconds) — on expiry the frame is dropped and its future removed from the ack FIFO so subsequent acks keep resolving the right entries ([#6709](https://github.com/flet-dev/flet/issues/6709), [#6710](https://github.com/flet-dev/flet/pull/6710)) by @ForsakenDurian.
* 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.
Expand Down
1 change: 1 addition & 0 deletions packages/flet/lib/flet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'package:flutter/material.dart' show Icons;
export 'src/controls/base_controls.dart';
export 'src/controls/control_widget.dart';
export 'src/extensions/control.dart';
export 'src/embedded_dart_bridge.dart';
export 'src/flet_app.dart';
export 'src/flet_app_errors_handler.dart';
export 'src/flet_backend.dart';
Expand Down
30 changes: 30 additions & 0 deletions packages/flet/lib/src/controls/flet_app_control.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';

import '../embedded_dart_bridge.dart';
import '../flet_app.dart';
import '../flet_app_errors_handler.dart';
import '../flet_backend.dart';
Expand All @@ -19,6 +20,31 @@ class FletAppControl extends StatefulWidget {

class _FletAppControlState extends State<FletAppControl> {
final _errorsHandler = FletAppErrorsHandler();
EmbeddedDartBridge? _dartBridge;

@override
void initState() {
super.initState();
// When this embedded app is addressed as `dartbridge://`, run it over an
// in-process dart_bridge channel instead of a socket. The native port is
// Dart-allocated, so we allocate it here and hand it to the host's Python
// via a `connect` control event — the host then serves that port with a
// FletDartBridgeServer. The embedded backend's send-retry loop covers the
// window until the server registers. Falls back to the URL transport when
// dart_bridge isn't available (web / desktop dev): _dartBridge stays null.
final url = widget.control.getString("url", "")!;
if (url.startsWith("dartbridge://") && embeddedDartBridgeConnector != null) {
final bridge = embeddedDartBridgeConnector!();
_dartBridge = bridge;
widget.control.triggerEvent("connect", bridge.port);
}
}

@override
void dispose() {
_dartBridge?.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -54,6 +80,10 @@ class _FletAppControlState extends State<FletAppControl> {
assetsDir: widget.control.getString("assets_dir", "")!,
errorsHandler: _errorsHandler,
extensions: FletBackend.of(context).extensions,
// In-process dart_bridge transport for `dartbridge://` embedded apps;
// null otherwise, so FletApp uses its URL-scheme channel factory.
channelBuilder: _dartBridge?.channelBuilder,
dataChannelFactory: _dartBridge?.dataChannelFactory,
args: widget.control.get("args") != null
? Map<String, dynamic>.from(widget.control.get("args"))
: null,
Expand Down
36 changes: 36 additions & 0 deletions packages/flet/lib/src/embedded_dart_bridge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'transport/data_channel.dart';
import 'transport/flet_backend_channel.dart';

/// An in-process `dart_bridge` transport allocated (on the Dart side) for one
/// embedded [FletApp].
///
/// `dart_bridge` channels are keyed by a Dart-allocated native port, so the port
/// must originate here and be handed to the Python side, which then serves it
/// with a `FletDartBridgeServer`. [port] is that native port; the embedded app's
/// backend talks over the channel via [channelBuilder] / [dataChannelFactory].
/// Call [dispose] when the embedded app is torn down.
class EmbeddedDartBridge {
final int port;
final FletBackendChannelBuilder channelBuilder;
final DataChannelFactory dataChannelFactory;
final void Function() dispose;

EmbeddedDartBridge({
required this.port,
required this.channelBuilder,
required this.dataChannelFactory,
required this.dispose,
});
}

/// Allocates an [EmbeddedDartBridge] for an embedded FletApp addressed as
/// `dartbridge://`.
///
/// Registered at startup by the build's `native_runtime.dart` (which owns
/// `serious_python`'s `PythonBridge`). Left null on web and on desktop/dev
/// builds where `dart_bridge` isn't available — there an embedded FletApp with a
/// `dartbridge://` url is expected to fall back to a socket URL instead.
typedef EmbeddedDartBridgeConnector = EmbeddedDartBridge Function();

/// Process-global hook; see [EmbeddedDartBridgeConnector].
EmbeddedDartBridgeConnector? embeddedDartBridgeConnector;
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# python-build release this flet pins. Keep in sync with serious_python's
# `pythonReleaseDate` (lib/src/python_versions.dart) — both should track the
# same python-build release.
PYTHON_BUILD_RELEASE_DATE = "20260720"
PYTHON_BUILD_RELEASE_DATE = "20260726"

RELEASE_DATE_ENV = "FLET_PYTHON_BUILD_RELEASE_DATE"
MANIFEST_PATH_ENV = "FLET_PYTHON_BUILD_MANIFEST"
Expand Down
11 changes: 11 additions & 0 deletions sdk/python/packages/flet/src/flet/controls/core/flet_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ class FletApp(LayoutControl):
Called when a connection or any unhandled error occurs.
"""

on_connect: Optional[ControlEventHandler["FletApp"]] = None
"""
Fires when the client allocates an in-process `dart_bridge` channel for this
embedded app (`url="dartbridge://"`). The event `data` is the Dart native
port the host must serve with a `FletDartBridgeServer` so the embedded app
connects over it instead of a socket.

Advanced / embedder use — hosts that run another Flet program in-process
(e.g. a gallery or preview) start their server on this port in the handler.
"""

on_python_output: Optional[EventHandler[FletAppOutputEvent]] = None
"""
Fires once per stdout/stderr write inside the embedded Pyodide app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ String initBridges(Map<String, String> envVars) {
"FLET_DART_BRIDGE_EXIT_PORT",
() => _exitBridge!.port.toString(),
);

// Let embedded FletApps (a Flet program run in-process by this app — e.g. a
// gallery/preview) use a dedicated in-process dart_bridge channel instead of a
// socket. Each embedded app mints its own PythonBridge here (Dart owns the
// native port); the flet package hands that port to the host's Python, which
// serves it with a FletDartBridgeServer. See EmbeddedDartBridge / FletApp.
embeddedDartBridgeConnector = () {
final bridge = PythonBridge();
return EmbeddedDartBridge(
port: bridge.port,
channelBuilder: ({
required FletBackendChannelOnPacketCallback onPacket,
required FletBackendChannelOnDisconnectCallback onDisconnect,
}) =>
_DartBridgeBackendChannel(bridge,
onPacket: onPacket, onDisconnect: onDisconnect),
dataChannelFactory: _PythonBridgeDataChannelFactory(),
dispose: () => bridge.close(),
);
};

return "dartbridge://${_bridge!.port}";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
flet:
path: ../../../../../packages/flet

serious_python: 4.3.6
serious_python: 4.4.1

# MsgPack codec used by the dart_bridge FletBackendChannel implementation
# in lib/main.dart — matches the wire format flet's existing socket
Expand Down
Loading