Integrate dart-bridge into flet-0.86 (in-process FFI transport, multi-version Python, native-mmap, DataChannel)#6601
Open
FeodorFitsner wants to merge 49 commits into
Open
Integrate dart-bridge into flet-0.86 (in-process FFI transport, multi-version Python, native-mmap, DataChannel)#6601FeodorFitsner wants to merge 49 commits into
FeodorFitsner wants to merge 49 commits into
Conversation
`ValueKey(controlKey.value)` produced `ValueKey<Object>(value)` because
`controlKey.value` is statically typed `Object`. Flutter's `ValueKey.==`
is runtimeType-strict, so `ValueKey<Object>('foo')` never equals
`ValueKey<String>('foo')` — making `find.byKey(Key('foo'))` /
`find.byKey(ValueKey('foo'))` in flutter_test fail to locate any
Flet-rendered control by user-assigned key.
Switch-dispatch on the runtime type so a String value yields
`ValueKey<String>`, int → `ValueKey<int>`, etc. This matches what
`Key('foo')` (factory for `ValueKey<String>('foo')`) and analogous
test-side constructions produce.
Repro: flet_example in flet-dev/serious-python on the dart-bridge
branch — its integration_test/app_test.dart with
`find.byKey(Key('increment'))` for an IconButton with
`key="increment"` was finding 0 widgets until this fix.
Adds a third transport (`FletDartBridgeServer` + Dart-side channel-builder
injection) that exchanges Flet's MsgPack protocol over the in-process
`dart_bridge` byte channel — the prebuilt-binary FFI bridge consumed by
serious_python plugins via `package:serious_python/bridge.dart`.
Coexists with the existing UDS / TCP socket transport. Activation:
- Python: `FLET_DART_BRIDGE_PORT=<port>` env var + `is_embedded()` true.
- Dart: pass `FletApp(channelBuilder: ...)` — the embedder constructs a
`FletBackendChannel` impl wrapping a `PythonBridge` and feeds it in.
`flet` package itself stays Python-independent: it does NOT depend on
`serious_python` or know about `PythonBridge`. The whole PythonBridge
wiring lives in the embedder's code (proven by a forthcoming
`flet_ffi_example` in serious-python). What lands here in `flet` is just
the seam.
Python side:
- New `flet/messaging/flet_dart_bridge_server.py` — `FletDartBridgeServer`
with the same protocol dispatch as `FletSocketServer`, lazy-imported so
non-embedded runs never load `dart_bridge`. Inbound: `__on_bytes`
enqueues payloads from the C-callback thread onto an asyncio.Queue
drained by `__inbound_loop`. Outbound: `send_message` calls
`dart_bridge.send_bytes(port, packb(...))`.
- `flet/app.py`: `run_async` selection block grows a third arm:
if is_embedded() and FLET_DART_BRIDGE_PORT: dart_bridge
elif is_socket_server: socket (existing)
else: web (existing)
- New helper `__run_dart_bridge_server` modelled on `__run_socket_server`.
Dart side:
- New `FletBackendChannelBuilder` typedef in
`transport/flet_backend_channel.dart`.
- `FletApp` accepts optional `channelBuilder`; `FletBackend` honours it in
`connect()` and skips the URL-scheme factory when present. URL-based
routing for socket / websocket / mock / Pyodide is unchanged.
Wire protocol — unchanged. Same `[ClientAction, body]` MsgPack frames,
same encoder/decoder, same Session dispatch. Only the byte transport
differs.
…t.dart (lets embedders implement channelBuilder)
The dart_bridge transport has no accept loop equivalent — start() registers a byte handler with libdart_bridge and returns immediately. Without an explicit wait, run_async() falls through to conn.close() as soon as main() returns, killing the bridge before any Dart-side frame can arrive. The embedded interpreter then exits even though the Flutter host is still running. Mirror the existing url_prefix/socket-server arm: wait on the terminate event when is_embedded() and FLET_DART_BRIDGE_PORT are both set.
Switches the production transport in `flet build`'s generated app from
TCP/UDS sockets to the in-process dart_bridge FFI channel that the
serious-python `dart-bridge` branch exposes. Web mode (websocket) and
developer mode (external Python process over TCP/UDS) stay unchanged —
PythonBridge only makes sense when the Python interpreter is embedded
in the same OS process as Flutter.
main.dart:
* Two long-lived PythonBridge instances created in prepareApp():
`_bridge` carries the MsgPack-framed Flet protocol; `_exitBridge`
is a dedicated outbound channel for Python's exit code (replaces
the legacy stdout-callback ServerSocket).
* pageUrl = `dartbridge://<port>`; env exports FLET_DART_BRIDGE_PORT
and FLET_DART_BRIDGE_EXIT_PORT. The Python flet package's app.py
picks up FLET_DART_BRIDGE_PORT and starts FletDartBridgeServer
instead of FletSocketServer.
* `_DartBridgeBackendChannel` (lifted from flet_ffi_example): wraps
PythonBridge as a FletBackendChannel — streaming msgpack decoder
on inbound, encoder + 30s retry loop on outbound. Injected into
FletApp via the `channelBuilder` parameter added in the flet PR.
* runPythonApp drops the ServerSocket setup; subscribes to
`_exitBridge.messages` and reuses the existing error-screen /
`exit(code)` handling unchanged.
* Dropped the now-unused `getUnusedPort` helper.
python.dart:
* Drops the `socket` callback channel and FLET_PYTHON_CALLBACK_SOCKET_ADDR.
* `flet_exit` posts the exit code as raw UTF-8 bytes via
`dart_bridge.send_bytes(FLET_DART_BRIDGE_EXIT_PORT, ...)`.
* stdout/stderr → FLET_APP_CONSOLE file redirection preserved (the
Dart side reads it for the error screen on `flet_exit(100)`).
pubspec.yaml:
* `serious_python` pinned to the dart-bridge branch via git ref —
1.0.1 on pub.dev predates PythonBridge. Swap to a version pin
once serious_python ships a release carrying the bridge API.
* Added `msgpack_dart: ^1.0.1` for the channel's wire codec.
Dev mode (--debug + page URL in args) still creates no bridges and
FletApp resolves transport via its URL-scheme factory; web mode reads
Uri.base unchanged.
Add a `path: src/serious_python` entry to the serious-python git dependency in sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml. This directs the package resolver to the subdirectory within the referenced repo (ref: dart-bridge) so the Dart package is loaded from src/serious_python instead of the repository root.
Brings in the multi-version bundled Python support from PR #6577. Conflict in `sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml` resolved by keeping dart-bridge's git-ref dependency on serious-python's `dart-bridge` branch + `msgpack_dart` (required by the in-process dart_bridge FFI transport). Swap back to `serious_python: ^2.0.0` once the FFI transport lands on a tagged serious-python release.
…ld vars
Mirror the serious-python registry bump:
* 3.12 row: Astral PBS date 20260610 (CPython 3.12.13 unchanged).
* 3.13 row: CPython 3.13.14, PBS date 20260610.
* 3.14 row: CPython 3.14.6, PBS date 20260610, Pyodide 314.0.0 GA.
* All three rows gain `python_build_date: "20260611"` for the new
date-keyed flet-dev/python-build release scheme.
The 3.13 wheel platform tag was also wrong — `pyodide-2025.0-wasm32`
where it should have been `pyemscripten-2025.0-wasm32` (the prefix
transition happened at Pyodide 0.28/0.29, not at 314.0). `flet build web
--python-version 3.13` would have failed to match Pyodide-built native
wheels. Fixed in the registry and called out in the 0.86.0 changelog.
`build_base.py` now exports two new env vars alongside the existing
`SERIOUS_PYTHON_VERSION` so the serious-python platform plugin build
scripts can construct the new URL form (`…/<YYYYMMDD>/python-*-<full>-*`):
* SERIOUS_PYTHON_FULL_VERSION → python_release.standalone
* SERIOUS_PYTHON_BUILD_DATE → python_release.python_build_date
Both are set in `package_env` (for `serious_python:main package`) and
`build_env` (for the subsequent `flutter build`).
Breaking-changes docs for 0.86: new 0.86.0 section in the index plus two
new guide pages covering (a) the default-Python bump 3.12 → 3.14 with
three pin options, and (b) the removal of `flet.version.pyodide_version`
/ `PYODIDE_VERSION` with the registry-lookup replacement. The dart_bridge
default-transport migration guide is intentionally not in this commit;
it'll be authored separately.
Publish docs tables (`publish/index.md`, `publish/web/static-website`)
updated to the new patch + Pyodide versions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds dedicated byte channels (`ft.DataChannel`) that let widgets exchange
bulk binary data (image frames, audio buffers, ML tensors) with their
Python counterpart without going through the MsgPack control protocol.
Architecture:
* `package:flet` exposes abstract `DataChannel` + `DataChannelFactory`.
Embedders inject a fast-path factory; absent that, a built-in
`ProtocolMuxedDataChannelFactory` muxes channel bytes over the active
Flet protocol transport.
* Python side: `ft.DataChannel` ABC with `_DartBridgeDataChannel`
(embedded native, dedicated PythonBridge) and `_ProtocolMuxedDataChannel`
(muxed fallback) impls. `Control.get_data_channel(id)` resolves a
channel allocated on the Dart side.
* Handshake: control-level event `data_channel_open` carrying
`{channel_name, channel_id}` — push-driven, no polling, no race.
Wire format change (breaking):
* All transports now prefix every packet with a 1-byte type
discriminator: `0x00` = MsgPack-encoded Flet protocol frame,
`0x01` = raw DataChannel frame (`[channel_id:u32 LE][bytes]`).
* Stream-oriented transports (UDS/TCP) gain a 4-byte little-endian
length prefix; message-oriented transports (WebSocket, postMessage,
dart_bridge) keep native message boundaries.
* `StreamingMsgpackDeserializer` removed — every inbound packet is one
complete MsgPack value, decoded via one-shot `msgpack.deserialize`.
Same simplification on the Python side: `Unpacker.feed` loops →
`msgpack.unpackb(payload)`.
Updated all four Connection subclasses (`FletSocketServer`,
`FletDartBridgeServer`, `flet_web.fastapi.FletApp`, `PyodideConnection`)
and all five Dart transports (socket, WebSocket, JavaScript/postMessage,
mock, JS stub) to the new framing. Pyodide outbound uses Transferable
ArrayBuffer for zero-copy across the worker boundary.
Three smoke tests in `packages/flet/test/transport/data_channel_test.dart`
cover factory allocation, inbound routing by channel id, and the outbound
muxed packet shape.
Replaces the `_invoke_method`-based `apply_full` / `apply_diff` / `clear` plumbing with a dedicated `DataChannel` carrying 1-byte-opcode frames (0x01=full PNG, 0x02=diff PNG, 0x03=clear). PNG bytes no longer pay MsgPack encode/decode — they flow at memory-bandwidth-class speed in embedded native mode and at near-bandwidth speed in dev/web modes (raw- byte frames muxed over the protocol transport). Backpressure follows the WebAgg pattern: Dart sends a 1-byte `[0xFF]` ack back over the same channel after each apply chain resolves; the canvas exposes `set_on_frame_applied(callback)` so `MatplotlibChart` clears `_waiting` only after Dart confirms the frame painted, mirroring mpl.js's `img.onload → waiting=false` flow. Without this gate, interactive drags pile up frames in the Dart-side queue and replay in a burst. The 3D example (`examples/.../matplotlib_chart/three_d/main.py`) adds a status bar showing avg full/diff frame size, total bytes transferred, sliding-window transfer speed, FPS, and per-stage latency (dart-side paint vs mpl-side render+idle) so users can see where time is spent. GPU / CPU strategy code in both State subclasses is unchanged — only the source of frames switched from `_invokeMethod(...)` to the channel listener.
…mpat
`flet build web` was failing to compile with errors like "Type 'Pointer'
not found" because the build template's `main.dart` unconditionally
imported `package:serious_python/bridge.dart` and
`package:serious_python/serious_python.dart`, both of which transitively
pull in `dart:ffi` types via `package:serious_python_platform_interface`.
`dart:ffi` isn't available in the web compile target.
Extract everything that touches `serious_python` into a separate
`native_runtime.dart`:
* `initBridges(envVars) → pageUrl` — creates the protocol + exit
PythonBridge instances and stamps env vars.
* `channelBuilder`, `dataChannelFactory` getters for the embedded
PythonBridge-backed transports.
* `runPython(...)` — wraps `SeriousPython.runProgram` + the exit-bridge
listener.
* `extractAppAssets(...)` — wraps `extractAssetZip`.
* The `_DartBridgeBackendChannel`, `_PythonBridgeDataChannel`, and
`_PythonBridgeDataChannelFactory` impls.
`main.dart` now uses a conditional import:
import 'native_runtime_stub.dart'
if (dart.library.ffi) 'native_runtime.dart' as nrt;
On web, the stub (`native_runtime_stub.dart`) is selected — every
entry point either returns null or throws `UnsupportedError`, and is
guarded behind `kIsWeb` so the stub is never reached at runtime. The
result: `flet build web` compiles cleanly without `dart:ffi` ever
entering the compile graph.
No behavior change on native (mobile/desktop) builds — they pick up the
real `native_runtime.dart` via the conditional and execute the same code
that lived in `main.dart` before.
Pyodide >= 0.29 (and 314.0.0, the Python 3.14 line) throws "Classic web
workers are not supported" inside any worker where `importScripts` is
callable. python-worker.js was spawned as a classic worker, so booting
the Python 3.13 / 3.14 lines surfaced a hard error before any user code
ran.
Switch to module workers across both the flet web client and the
`flet build` template:
* `new Worker(url, { type: "module" })` — module workers don't expose
`importScripts`, so Pyodide's check passes.
* `importScripts(pyodideUrl)` → `const { loadPyodide } = await
import(pyodideUrl)` — the dynamic-import form module workers must
use.
* All `pyodideUrl` defaults flip from `pyodide.js` to `pyodide.mjs` —
the ES-module variant has the named export the dynamic import expects.
URL injection paths:
* `flet publish` / `flet run --web` go through `patch_index.py`, which
now injects `pyodide.mjs` URLs (both CDN and `--no-cdn` branches).
* `flet build web` uses the cookiecutter template's index.html, which
was hardcoded at `/pyodide/pyodide.js` regardless of `--no-cdn`.
Replace with a Jinja conditional that honors `cookiecutter.no_cdn`
and uses the new `cookiecutter.pyodide_version` variable for the
jsdelivr CDN URL. `build_base.py` populates `pyodide_version` from
the resolved `python_release.pyodide`.
Forward-compatible across all three supported Pyodide lines:
0.27.7 (Python 3.12), 0.29.4 (Python 3.13), 314.0.0 (Python 3.14).
Older lines accept module workers too; 0.29+ require them.
* CHANGELOG: new features (DataChannel API), improvements (length-prefix framing + type-byte discriminator, StreamingMsgpackDeserializer removed), breaking changes (wire format on stream transports, mixed flet versions across `flet run` CLI and runtime no longer supported). * New breaking-changes guide `data-channel-protocol-upgrade.md` — migration notes for users with custom backends speaking the Flet protocol, plus a heads-up for anyone subclassing `MatplotlibChartCanvas` (the Dart-side `_invokeMethod` handler no longer fires). * Add the new guide to the 0.86.0 entry in the breaking-changes index.
…ayBuffer The worker→main `postMessage` path was structured-cloning every bulk payload (matplotlib PNG frames, etc.) — measurable cost at ~300 KB per frame. Switch to Transferable: extract the Uint8Array's underlying ArrayBuffer and pass it in the second argument to postMessage. Main thread receives the buffer with ownership transferred, no copy. The matching main→worker (Dart→Python) direction already used Transferable since the DataChannel landing. Both directions are now zero-copy across the worker boundary on Pyodide. This does not move the matplotlib bottleneck — that's WASM-compute-bound on mplot3d — but trims a few ms of structured-clone cost per frame and makes the perf budget closer to what the dart_bridge embedded path delivers natively.
The sync `apply_full` + side-channel `_on_frame_applied` callback was losing matplotlib "draw" events in pyodide mode. Sequence: 1. `_receive_loop` reads frame bytes, calls `apply_full(bytes)` — sync, returns immediately. 2. Loop iterates, reads next event from `_receive_queue`. 3. Next event is a `"draw"` notification matplotlib emitted just after the previous frame (figure dirty again from mouse drag). 4. Gate check: `_waiting=True` (ack hasn't arrived from Dart yet) → **drop the event**. 5. Ack arrives 200+ ms later, `_waiting=False`, but the queue is empty and matplotlib doesn't re-emit "draw" until next mouse event. Result in pyodide: ~1.5 fps observed, vs the 0.85 `_invoke_method` implementation's much higher rate. The 0.85 pattern wasn't faster because it lacked an ack — it had one (the INVOKE_METHOD reply). It was faster because `await self._invoke_method(...)` **blocked the `_receive_loop`** during the round-trip, so matplotlib events queued naturally in `_receive_queue` and were processed in order after the await returned, rather than being eagerly drained against a stale gate. Fix: re-introduce the await pattern at the canvas level. * `MatplotlibChartCanvas.apply_full / apply_diff / clear` are now async. Each enqueues a per-frame `asyncio.Future`, sends the channel packet, and awaits the future. * `_on_dart_message` resolves the head future when `[0xFF]` arrives. * `MatplotlibChart._receive_loop` awaits each `apply_*` call — matplotlib events that arrive during the wait stay queued and are processed after the ack returns. Same behaviour shape as 0.85's `_invoke_method` round-trip, but over the DataChannel transport (no msgpack on the bulk payload). * `set_on_frame_applied(cb)` is preserved as a pure observer callback for instrumentation (e.g. the 3D example's stats panel) — no longer load-bearing for backpressure. The 3D example's `apply_full` / `apply_diff` wrappers updated to `async def` + `await` accordingly.
The multi-version Python PR (#6577) removed flet.version.pyodide_version but the 'Get Pyodide version' step still read it, failing every 'Build Flet Client for Web' run. Resolve the version from the flet_cli.utils.python_versions registry instead (default release's Pyodide), and replace the hand-rolled tarball + wheel downloads with flet_cli.utils.pyodide.ensure_pyodide — the hardcoded micropip-0.8.0/packaging-24.2 filenames would have silently broken on the new Pyodide line (3.14's lock resolves micropip 0.11.1), since curl without -f writes 404 pages into the .whl files. Cherry-picked from 2d8f4a1 on fix-android-arch-filtering. Co-authored-by: ndonkoHenri <robotcoder4@protonmail.com>
…link The 0.86 protocol-framing breaking-change guide linked to a DataChannel API reference page that doesn't exist yet — there's no extending-flet/ folder, and no DataChannel doc has been authored. Docusaurus' broken-link scan failed the docs build on every push. Replace the link with prose pointing at the data_channel.py module docstring; dedicated reference pages can land in a follow-up once the API doc generator covers it.
GitHub Actions emitted Node.js 20 deprecation warnings on every job in run 27457389406. Node 20 will be removed from runners 2026-09-16. Bump the affected actions to their latest Node 24 majors across all workflows: - actions/checkout@v4 → v6 - actions/setup-node@v4 → v6 (v6 limited auto-cache to npm, the website uses yarn via corepack — no caching behavior change) - actions/upload-artifact@v4 / v5.0.0 → v7 - actions/download-artifact@v4 → v8 - astral-sh/setup-uv@v6 → v8.2.0 (v8 dropped the major @v8 tag for supply-chain reasons, full tag required) - dart-lang/setup-dart@<old SHA> → v1.7.2 All six actions' action.yml now declare `runs.using: node24`.
`ValueKey(controlKey.value)` produces `ValueKey<Object>` because
`controlKey.value` is statically typed Object. Flutter's `ValueKey.==`
is runtimeType-strict, so `ValueKey<Object>('foo')` never equals
the `ValueKey<String>('foo')` that ControlWidget assigns to the
rendered widget — making `find_by_key("foo")` from Python tests
find 0 widgets.
Mirrors the fix already applied in control_widget.dart (7367050).
Switch-dispatch on the runtime type so String → ValueKey<String>,
int → ValueKey<int>, etc.
Resolves the cascade of "RangeError: no indices are valid: 0" and
"assert 0 == 1" failures across apps, controls/core, controls/material,
controls/cupertino, and controls/theme integration suites.
#6545 renamed 131 example folders (mostly basic/ → descriptive control name, plus example_1/2/3, nested_themes_1/2 collapsing, and removing the basic/ wrapper where there was only one example) but the matching imports in packages/flet/integration_tests/examples/ were never updated. Test collection failed with ModuleNotFoundError on every affected suite (examples/apps, examples/extensions, and examples/controls/{core,cupertino,material}). Rewrites the 45 test files referencing those modules to the new paths derived from the rename history of commit 1b2e914.
…s from it Add a --json flag to 'flet --version' that emits a machine-readable document (Flet/Flutter versions, supported Python/Pyodide table, Linux build deps). CI workflows and the publish docs now read it via jq instead of importing Flet internals with 'python -c'. Move the canonical Linux apt dependency list from flet.utils.linux_deps (runtime package) to flet_cli.utils.linux_deps (build tooling), where it sits next to python_versions.py and is a same-package import for the CLI.
GitHub is redirecting windows-latest to windows-2025-vs2026 by June 15, 2026. Pin the label explicitly in the Flet Build Test and Build & Publish workflows to silence the redirect notice and make the image deterministic.
Update pubspec.yaml dependency for flutter_secure_storage from fixed 10.0.0 to caret ^10.0.0, allowing compatible minor/patch updates instead of pinning to a single patch version. This lets the package accept backwards-compatible releases without manual changes. Fix #6586
Drop flet's hand-mirrored SUPPORTED_PYTHON_VERSIONS table and load the supported Python/Pyodide/dart_bridge set from python-build's date-keyed manifest.json — the single source of truth shared with serious_python. - python_versions.py: pin one PYTHON_BUILD_RELEASE_DATE; fetch that release's manifest.json (cached immutably under ~/.flet/cache/python-build, offline fallback to cache) and parse it lazily. Module constants become get_supported_python_versions()/get_default_python_version(); resolution logic unchanged. Dev/CI overrides: FLET_PYTHON_BUILD_RELEASE_DATE, FLET_PYTHON_BUILD_MANIFEST. - flet build: pass only SERIOUS_PYTHON_VERSION; serious_python derives the full version, build date, and dart_bridge version from its committed snapshot. Drops the SERIOUS_PYTHON_FULL_VERSION/SERIOUS_PYTHON_BUILD_DATE exports. - flet --version: drop the Python/Pyodide matrix (stays offline); --json keeps flet/flutter/linux_dependencies. - ci.yml: read the default Pyodide version via the manifest-backed resolver instead of jq over `flet --version --json`. - Docs: update the removed-pyodide-version-export guide + CHANGELOG to the new accessors; document the pin in CONTRIBUTING. - Add offline tests driven by FLET_PYTHON_BUILD_MANIFEST.
…ression)
screen_brightness_macos 2.1.3 ("Fix: swift package manager warning") ships a
Package.swift declaring macOS 10.11, below FlutterFramework's 10.15 SPM floor,
so `flutter build macos` fails to resolve with Swift Package Manager enabled.
Pinning the app-facing screen_brightness alone doesn't help — the federated
macOS implementation is separately versioned. Override the impl to the last
good 2.1.2 in both the build template and the client app.
Upstream: aaassseee/screen_brightness#99
Switching --python-version (or requires-python) between builds left the previous version's compiled bytecode in the reused build directory's native bundles (stdlib/site-packages .pyc), crashing the app at runtime with `ImportError: bad magic number`. Record the resolved Python short version in the build dir and, when it changes, wipe the build dir so the native bundles are regenerated for the new interpreter.
The earlier 0.86.0 entries described `flet --version` listing the Python/Pyodide matrix and `--version --json` carrying the full table — both removed when version resolution moved to python-build's manifest. Update those entries to the shipped behavior (flet/flutter only; --json drops the Python table), add the manifest-model entry (constants -> get_supported_python_versions(), `flet build` forwards only SERIOUS_PYTHON_VERSION), and fix the stale "central registry" / `serious_python >= 2.0.0` references.
Move the pinned Flutter from 3.41.7 to 3.44.2 (.fvmrc; CI/Docker/version.py
derive from it) and absorb the 3.44 breaking changes.
- Android built-in Kotlin migration: the Flet client and the `flet build`
template no longer apply the Kotlin Gradle plugin themselves (Flutter applies
kotlin-android); Java 11 -> 17, `kotlinOptions` -> the `kotlin { compilerOptions }`
DSL. Client AGP 8.12.1 -> 8.11.1, KGP 2.1.0 -> 2.2.20, Gradle 8.13 -> 8.14,
hardcoded NDK -> flutter.ndkVersion.
- No Dart changes needed: flet already uses the modern Color/WidgetState/Material3/
SharePlus APIs and the page-transition builders survived the 3.44 reorg.
Verified: flutter analyze clean (packages/flet + client), packages/flet tests
pass, flet --version reports 3.44.2, client web (wasm) build succeeds.
Flutter 3.44.2 slightly changed Material/Cupertino date-picker rendering, so the *_locale screenshot goldens drifted past the 99% similarity threshold. Regenerated on macOS (matching the macos-26 CI runner) via FLET_TEST_GOLDEN=1; re-verified at 100% similarity without golden mode. - controls/material: date_picker/locale, date_range_picker/locale - controls/cupertino: cupertino_date_picker/locale - examples/controls/material: date_picker/custom_locale, date_range_picker/custom_locale
`run_async`'s dart_bridge branch wraps the `FletDartBridgeServer` in a
new `_DartBridgeServerHandle` facade that swaps the underlying
connection in place when a Dart VM restart signal arrives from
libdart_bridge.
Trigger: on Android, the OS may keep the OS process alive across a
back-button quit and restart only the Dart VM on re-launch. The new
VM's `PythonBridge` allocates fresh native ports; the running Python
still has handlers on the (now-dead) ports from the previous VM.
libdart_bridge 1.3.0's `dart_bridge.add_session_restart_handler` fires
the callback registered here with `{label: new_port}`, this rebuilds
`FletDartBridgeServer` on the new "protocol" port, then closes the
stale one. User-level Python state (singletons, in-memory data) is
preserved; the Flet session is rebuilt from REGISTER_CLIENT.
Soft-binding: `add_session_restart_handler` is looked up with
`getattr` so older libdart_bridge that doesn't expose the API still
loads — process-reuse is just unsupported on those, matching today's
behavior.
native_runtime.dart `initBridges`:
* Unconditionally calls `DartBridge.instance.signalDartSession(...)`
with the new `{protocol, exit}` port map after allocating the bridges.
* On fresh start: libdart_bridge has no embedded Python yet → no-op.
* On Android process reuse: fires every Python session-restart handler
registered by flet.app + python.dart bootstrap, so the running Python
rebinds to the new ports.
* Exposes `pythonAlreadyRunning` (forwards `DartBridge.isPythonInitialized`)
so `main.dart` can skip `SeriousPython.runProgram` on reuse — the
embedded Python is already running with the user's `main()`, and
trying to run it again would either crash (pre-1.3.0) or no-op-return
immediately (1.3.0+).
main.dart `runPythonApp`:
* Checks `nrt.pythonAlreadyRunning` and parks on an unresolved future
when true. Keeps the existing `FutureBuilder` plumbing intact while
the just-restarted Dart VM picks up its UI session through the
already-rewired FletDartBridgeServer.
Requires libdart_bridge >= 1.3.0 for the actual process-reuse path to
fire; against older binaries the soft-lookup in serious-python's
DartBridge bindings drops `signalDartSession` to a no-op and
`pythonAlreadyRunning` returns false — degrades to the pre-1.3.0
behavior (no process-reuse support, no crash either).
…trap Two related changes to the bundled bootstrap script: 1. `_exit_port` becomes a one-element list so the session-restart handler can update it in place on Android process reuse. Without this, after a Dart VM restart, `flet_exit` would still post the exit code to the OLD (now-dead) PythonBridge port and the host process would never receive the signal. Subscribe to `dart_bridge.add_session_restart_handler` (>= 1.3.0) to mutate `_exit_port[0]` whenever the new VM signals fresh ports. `getattr` lookup keeps older libdart_bridge working — process-reuse is just unsupported on those, exit-code routing unchanged. 2. `sys.stdout` / `sys.stderr` become `_TeeWriter` instances that duplicate writes to BOTH the libdart_bridge native log writer (logcat / os_log / stderr, installed at Py_Initialize by 1.3.0+) AND the existing `out_file` (the error-screen capture file). Previously the script overwrote sys.stdout/stderr with just `out_file`, which silenced the new native log path entirely on Android — the whole point of the stdio install in libdart_bridge. On older libdart_bridge without the native install, the tee falls back to writing through the default text streams + file, so error capture works the same as today and there's no console output regression.
Python's `print(x)` is internally two writes: `write(x)` then
`write("\n")`. libdart_bridge's native-log writer strips the trailing
newline before calling `__android_log_write`, so the standalone "\n"
write becomes an empty string and produces a blank logcat row after
every print:
I Hello, Flet!
I
I Another line of output.
I
I And another one.
I
The file half of the tee still receives writes raw — `out_file` keeps
byte-for-byte parity with what Python wrote so the error-screen
capture matches a plain `python` console run.
The native half now accumulates until "\n", emits the line without
the trailing newline, and skips purely empty lines (so `print()` with
no args also stays out of the log instead of producing a blank
entry). `flush()` drains any pending partial line in case the user
app calls it mid-line.
Result is one logcat row per logical print line — what the user
expects.
Add a local Swift package product (FlutterGeneratedPluginSwiftPackage) to the macOS Xcode project and Runner scheme, including a pre-build action to run Flutter's macos_assemble.sh prepare step to ensure the framework is prepared. Update Windows plugin registration to use the screen_brightness_windows C API header and registration function. Bump screen_brightness in packages/flet and remove prior direct overrides from client and template pubspecs so the project uses the newer package resolution and avoids the previous workaround for macOS/SVM target issues.
The web stub of native_runtime didn't expose the new pythonAlreadyRunning
getter introduced for the Android process-reuse path, so main.dart's
unconditional check `if (nrt.pythonAlreadyRunning)` failed to compile on
web targets:
lib/main.dart:297:11: Error: Undefined name 'pythonAlreadyRunning'.
if (nrt.pythonAlreadyRunning) {
^^^^^^^^^^^^^^^^^^^^
Add the stub getter returning false — there's no embedded CPython on
web (Pyodide runs via the JavaScript channel, not dart_bridge), so
"already running" is by definition never true. Symmetric with how the
stub treats other native-only entry points.
…oid-extract-packages (#6595) * build(android): consume serious_python native-mmap packaging + extract-packages option - Build template: point serious_python git ref at android-native-mmap; drop the stale packaging{jniLibs{useLegacyPackaging=true; keepDebugSymbols}} block from the Android app build.gradle.kts (native modules now load mmap'd from the APK; modern packaging at minSdk 23+ is all that's needed). - flet-cli: add --android-extract-packages CLI flag + [tool.flet.android].extract_packages (cross-platform [tool.flet].extract_packages fallback), a built-in ANDROID_DEFAULT_EXTRACT_PACKAGES set (certifi), merged and exported as SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES for the serious_python package step. Mirrors the existing --source-packages -> SERIOUS_PYTHON_ALLOW_SOURCE_DISTRIBUTIONS flow. * build(android): route extract-packages env to the flutter build + docs The serious_python_android Gradle split (which partitions site-packages into the stored sitepackages.zip vs the extracted extract.zip) runs during `flutter build`, not the `serious_python package` step. Set SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES on build_env (resolved once into self.android_extract_packages) so the default set (certifi) and user list actually reach the split — previously it was set on the package step's env and silently ignored, leaving extract.zip empty. Add the "Android extract packages" publish-docs section (resolution order, CLI + pyproject example), a CHANGELOG entry, and allowlist `certifi` in typos. Verified end-to-end on an arm64 emulator via both `flet build apk` and `flet build aab` (bundletool split install): numpy mmap'd from the APK, certifi extracted to disk, flet icons.json read from the stored zip, flet.version resolved, app launched. * build(android): empty the default extract set — certifi is zip-safe certifi reads cacert.pem via importlib.resources.as_file(), which extracts it to a temp file on demand, so certifi.where() works from inside the stored sitepackages.zip (verified: imported via zipimport, where() returns a valid 234KB cacert.pem). It does not need to ship extracted. Make ANDROID_DEFAULT_EXTRACT_PACKAGES empty — the common data-bundling packages use importlib.resources (zip-safe). The --android-extract-packages / [tool.flet.android].extract_packages mechanism stays for genuinely path-hungry packages (those reading data via __file__ / pkg_resources). Update docs and CHANGELOG accordingly.
Relocate the "Android extract packages" section from publish/index.md into publish/android.md so Android-specific guidance lives on the Android doc. The moved content explains path-hungry packages, zipimport limitations, the resolution order (CLI flag, [tool.flet.android], [tool.flet]) and includes examples for the CLI and pyproject.toml. Removes the duplicate section from the index to keep platform-specific docs consolidated.
Change the serious_python git dependency ref from 'android-native-mmap' to 'dart-bridge' in sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml. This ensures the template uses the dart-bridge variant of serious-python, aligning with the dart_bridge FletBackendChannel implementation.
… opt-out) (#6598) * build: compile app + packages to .pyc by default flet build / flet publish now compile the app and installed packages to bytecode by default (previously off). Shipping .pyc removes per-launch recompilation, which is a large cold-start win on mobile where pure Python is imported in place from a stored zip and can't cache bytecode back to disk. - --compile-app / --compile-packages use argparse.BooleanOptionalAction, adding --no-compile-app / --no-compile-packages; the positive flags still work. - [tool.flet.compile].app / .packages default to true (get_bool_setting default flipped False -> True); precedence unchanged (CLI > per-platform > global). - Verified a compiled `flet build web` bundle: app ships .pyc only and the magic matches Pyodide's CPython 3.14 (standalone 3.14.6 compile vs Pyodide 3.14.0 — magic is frozen across a minor version's patch releases). Docs: publish "Compilation and cleanup" section updated, new breaking-changes guide + index entry, CHANGELOG breaking-changes entry. * docs: update summary to clarify that only `flet build` compiles to .pyc by default
…(mobile cold start) (#6597) * perf(flet): lazily import the public API (PEP 562 __getattr__) `import flet` previously executed the whole ~270-module package eagerly (~240 flet modules, ~2.0s on a mid-range Android device from .pyc). Move the entire public surface behind a generated `_LAZY = {name: module}` map and a module-level `__getattr__` that imports each name on first access and caches it into globals(); the real imports stay under `if TYPE_CHECKING:` so type checkers / IDEs / `__all__` are unaffected. `__version__` stays eager. Apps now load only the modules they touch. On the counter example (moto g15), PY_BOOT -> UI built drops 2.10s -> 0.82s (import flet alone 2.02s -> 0.15s; 22 flet modules at import vs 240). Full pytest suite passes (210 passed, 9 skipped); resolved objects are identical to the eager imports by construction (the map is generated from the original import statements). * perf(flet): defer Cupertino + deprecated-service imports off the Page/View hubs Page/View/base_page are loaded by every app and eagerly pulled clusters a typical Material app never touches: - Cupertino app bar / navigation bar: only referenced in `appbar` / `navigation_bar` field & property annotations. Move the imports to TYPE_CHECKING and string-quote those (non-event) annotations so they are not evaluated at class-definition time. control_event only resolves event-handler fields, so quoting these is safe. - 4 deprecated page service properties (BrowserContextMenu, SharedPreferences, Clipboard, StoragePaths): construct their service on demand via a function-local import instead of importing the whole set at module load. Counter example: flet modules at first-frame 114 -> 108 (cupertino 4 -> 2, services 7 -> 3). Full pytest suite still passes (210 passed, 9 skipped). Note: module-wide `from __future__ import annotations` is intentionally NOT used here — flet's control_event resolves event-handler annotations at runtime via get_args(), which requires real type objects, not PEP 563 strings. * perf(flet): defer the auth subsystem off Page import Page eagerly imported the whole flet.auth subsystem (7 modules) even though a typical app never calls Page.login(). The eager edges were: module-level Authorization / OAuthProvider imports, the AuthorizationImpl default (AuthorizationService / Authorization chosen at import via is_pyodide()), and AT = TypeVar("AT", bound=Authorization). Defer all of it: - Authorization / OAuthProvider imports -> TYPE_CHECKING; their annotations are string-quoted so they aren't evaluated at class-definition time. - TypeVar bound -> "Authorization" (string forward ref). - AuthorizationImpl default -> a lazy _default_authorization_impl() helper that imports AuthorizationService / Authorization on first Page.login() call; the login() authorization parameter defaults to None and resolves to it. Counter example: flet modules at first-frame 108 -> 101 (flet.auth 7 -> 0). Full pytest suite passes (210 passed, 9 skipped), including test_auth_lazy_imports. * perf(flet): defer the components/hooks subsystem off the core import path Page / BasePage / Session pulled the whole components + hooks subsystem (9 modules) at import even though most apps use no components or hooks: - public_utils.unwrap_component (called by Page/BasePage on hot paths) imported Component at module load just for an isinstance check. Import it lazily and cache it, so importing the helper no longer pulls component.py. - Page.render / render_views import Renderer function-locally (only component apps call these). - Session.schedule_effect's EffectHook parameter is annotation-only -> move to TYPE_CHECKING and string-quote (Session never constructs EffectHook; the hook is passed in by the component runtime). Counter example: flet modules at first-frame 101 -> 94 (flet.components 9 -> 2, the two remaining being the empty package and the lightweight public_utils). The components/hooks machinery now loads on first actual use. Full pytest suite passes (210 passed, 9 skipped).
Match the `v<major>-<minor>-<patch>-` filename convention (introduced in the flet-clean PR for the 0.85.0 guides). Prefix the four guides added on dart-bridge for 0.86.0 with `v0-86-0-`: - compile-on-by-default - data-channel-protocol-upgrade - default-bundled-python-3-14 - removed-pyodide-version-export Update the breaking-changes index links and the two CHANGELOG links (compile-on-by-default, data-channel-protocol-upgrade) to the new paths.
Three user-facing changes done in the dart-bridge branch had no CHANGELOG entry. Add them: - New feature: in-process Python transport (dart_bridge FFI) — the third protocol transport alongside UDS/TCP sockets, the FletApp(channelBuilder:) seam serious_python uses to embed Python in-process, the build template's migration off sockets, and the Android session-restart rebinding. - Improvement: lazy `import flet` (PEP 562 __getattr__), ~2.0s -> ~0.15s on a mid-range Android device (#6597). - Bug fix: ValueKey value-type preservation so find.byKey / find_by_key locate controls by user-assigned key (Dart + Python tester).
Contributor
There was a problem hiding this comment.
Sorry @FeodorFitsner, your pull request is larger than the review limit of 150000 diff characters
Deploying flet-website-v2 with
|
| Latest commit: |
706e452
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2155efbe.flet-website-v2.pages.dev |
| Branch Preview URL: | https://dart-bridge.flet-website-v2.pages.dev |
Practical reference for extension authors who need to move bulk binary
data (image frames, audio buffers, ML tensors) between Python and Dart
without the MsgPack control protocol overhead. Source distillation of
`dedicated-data-channels.md` in flet-dev/serious-python, scoped to what
an extension author actually needs to ship a working widget:
* When to reach for DataChannel vs. when the regular property protocol
is already fast enough.
* Python-side API: `on_data_channel_open` event handler +
`Control.get_data_channel(channel_id)` to attach.
* Dart-side API: `FletBackend.of(context).openDataChannel()` +
firing the `data_channel_open` control event with
`{channel_name, channel_id}`.
* Multi-channel pattern (`channel_name` dispatch).
* Threading + backpressure caveats — `on_bytes` runs under the GIL on a
transport thread, receive queues are unbounded by default.
* Cross-links to the full design doc, the breaking-change protocol
guide, and the MatplotlibChartCanvas reference impl (both halves).
Deliberately omits wire-format, Isolate-scope, and throughput-table
material — those live in `dedicated-data-channels.md` for anyone
needing the deeper picture.
Placed between "Customize properties" and "Examples" since it's
opt-in advanced functionality most extension authors won't need.
ndonkoHenri
reviewed
Jun 18, 2026
Contributor
There was a problem hiding this comment.
These breaking changes pages need to equally be added to the sidebar.yml. A section was added for v0.86.0, which you will find when merging flet-86 branch into this one.
Lift the Android-ABI fixes from #6578 onto the dart-bridge branch, adapted to the manifest-driven `PythonRelease`. Per-version ABI set is now sourced from python-build's `pythons.<short>.android_abis` (release 20260618+); flet keeps no hardcoded ABI table. - `PythonRelease` gains `android_abis: tuple[str, ...]`, populated from the manifest. Pinned `PYTHON_BUILD_RELEASE_DATE` bumped 20260614 -> 20260618 (the python-build release that introduced the field). - New `utils/android.py` with `ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM`, `flutter_target_platforms()`, `excluded_android_abis()`. - `build_base.py`: `action="extend"` on `--arch`, `--source-packages`, `--permissions` (repeated flags now accumulate instead of overwriting). Android-only `--arch` validation: invalid ABI -> clean error; ABI not in `python_release.android_abis` -> clean error pointing at the bundled Python; empty defaults to the supported ABI list. Cookiecutter context gains `android_excluded_abis`. `--arch` to serious_python is comma-joined (Dart multi-option). `resolve_output_path()` factored out so `build.py`'s pre-build cleanup can share the path resolution. - `build.py`: forwards requested ABIs to Flutter as `--target-platform android-arm,android-arm64,android-x64` (so `--split-per-abi` builds only the requested splits), wipes each platform's output dir before the flutter build so stale artifacts don't leak into the user's output, fixes a `self.target_platform in "apk"` substring-vs-equality bug uncovered by the new code path. - Cookiecutter Android template: `packaging.jniLibs.excludes += listOf("lib/<abi>/**", ...)` block, gated on `cookiecutter.options.android_excluded_abis`. Needed because AGP unions defaultConfig + buildType `ndk.abiFilters`, and other plugins' jniLibs slip past `--target-platform`. - CHANGELOG breaking-change + two bug-fix bullets; android.md gets per-version ABI support, PEP 738 note, default-by-bundled-Python wording. Refs: #6567, #6578 by @ndonkoHenri. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls 6 commits from the flet-0.86 release branch: * feat(FilePicker): compression_quality + cancel_upload_on_window_blur on pick_files() (#6573) * fix(android): pickFirsts libc++_shared.so (#6571) * chore: docs + code improvements across packages/flet Dart controls (#6589) * fix(typing): explicit handler signatures in PubSubClient (#6564) * feat(cli): flet clean command + deprecate --clear-cache (#6590) * fix(flet-secure-storage): bump flutter_secure_storage 10.0.0 -> 10.3.1 (#6586) Conflicts and resolutions: * sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts flet-0.86's #6571 reinstates useLegacyPackaging + keepDebugSymbols + pickFirsts inside packaging.jniLibs. dart-bridge intentionally dropped useLegacyPackaging / keepDebugSymbols because serious_python's new native-mmap loader makes them redundant (CHANGELOG 0.86.0 Improvements). The libc++_shared.so collision is a different problem (multi-plugin jniLibs duplication, not Python extraction), so keep just the pickFirsts block. Restructured the packaging block I added in the previous commit so pickFirsts is unconditional and excludes stays gated on android_excluded_abis. * CHANGELOG.md Merged the new-features and bug-fixes sections preserving entries from both branches: dart-bridge's DataChannel + in-process dart_bridge + multi-version Python (with the corrected 314.0.0 GA / sp >= 3.0.0 blurb that supersedes flet-0.86's 314.0.0a2 / sp >= 2.0.0 stub), plus flet-0.86's flet clean, compression_quality, libc++_shared fix, PubSubClient typing, and FilePicker pick_files entry. Documentation subsection from flet-0.86 added. * sdk/python/packages/flet-secure-storage/.../pubspec.yaml Took flet-0.86's pinned `flutter_secure_storage: 10.3.1` over dart-bridge's `^10.0.0` caret range. * website/docs/updates/breaking-changes/index.md Kept dart-bridge's #### Breaking changes subsection and added flet-0.86's #### Deprecations subsection beneath it. * tests/test_python_versions.py (post-merge follow-up, not a conflict) Updated the local fixture manifest to carry android_abis on every row. Required because the manifest-driven PythonRelease parser no longer falls back to an inferred ABI list — the fixtures must reflect the same schema as python-build's 20260618+ releases. Verification: ruff clean on touched files; 62 flet-cli unit tests pass; imports succeed for build_base, build, clean, python_versions, android. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #6601 review (@ndonkoHenri) flagged that the four breaking-change guides added on dart-bridge weren't wired into website/sidebars.yml's v0.86.0 section (only flet-0.86's clear-cache deprecation entry was). Added them in the same order as updates/breaking-changes/index.md: breaking changes first, then deprecations.
Create a new blog post stub for the Flet 0.86.0 release at website/blog/2026-07-01-flet-v-0-86-release-announcement.md. The file (author: feodor, tag: releases) includes a short summary highlighting routing and dialogs, smoother video, real-time audio, and bug fixes, plus a CTA to GitHub Discussions and Discord; body content is currently marked TBD.
Replace the git-based serious_python dependency in the Python template pubspec with a version pin (3.0.0). Removes the git url/ref/path block that referenced the dart-bridge branch, simplifying the template and using the published package instead of an external repo reference.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the full
dart-bridgeline of work (43 commits) intoflet-0.86for the 0.86.0 release.What this lands
Architecture
dart_bridgeFFI) — a third Flet protocol transport alongside UDS/TCP sockets;serious_python≥ 3.0.0 embeds Python in-process (no localhost socket), with Android session-restart rebinding. Theflet buildtemplate migrates off sockets.ft.DataChannel— high-throughput binary byte channels between Dart and Python, plus unified length-prefixed protocol framing with a 1-byte type discriminator across all transports.Build / packaging
serious_python(Multi-version Python support (3.12 / 3.13 / 3.14) #6577).useLegacyPackaging; new--android-extract-packages(build(android): consume serious_python native-mmap packaging + --android-extract-packages #6595)..pycby default with--no-compile-*opt-out (build: compile app + packages to .pyc by default (with --no-compile-* opt-out) #6598).import flet(perf(flet): lazy public-API imports + defer eager subsystem clusters (mobile cold start) #6597).Fixes / docs
flet-0.86already hasflet clean/--clear-cachedeprecation (#6590) and the secure-storage bump (#6586). These overlap with dart-bridge, so resolve by combining both sides:CHANGELOG.md— keep both 0.86.0 entry sets (flet-clean / clear-cache + dart-bridge items).website/docs/updates/breaking-changes/index.md— union the 0.86.0 link lists; both branches use thev0-86-0-prefix convention, so the four dart-bridge guides +v0-86-0-deprecated-clear-cache-flagshould all be listed.build_base.py— flet-0.86 deprecated--clear-cache; dart-bridge added compile/extract-packages wiring. Keep both.cli.py,.github/workflows/ci.yml,website/docs/publish/index.md(minor).The breaking-changes guide files don't collide (different filenames); only the index + CHANGELOG need merging.