Multi-version Python support (3.12 / 3.13 / 3.14)#6577
Conversation
Delete generated web/canvaskit build artifacts from sdk/python/templates/build/{{cookiecutter.out_dir}} (canvaskit.js, .symbols, .wasm and chromium/skwasm variants). These are built/WebAssembly output files and have been removed from the template to avoid committing generated binaries and reduce repository/template size.
Add a `python_versions` registry that maps each supported short Python
version to its CPython-standalone build, Pyodide release, and Emscripten
wheel platform tag. `flet build` and `flet publish` resolve the version
in this order: `--python-version X.Y` (new flag) -> `[project].requires-
python` (highest matching stable; pre-release rows are skipped unless an
explicit specifier like `==3.15.*` opts in) -> default `3.14`.
`build_base.py` threads the resolved version through to serious_python
(via `--python-version` and `SERIOUS_PYTHON_VERSION`) and the Flutter
build env, and exposes it to the cookiecutter template as
`cookiecutter.options.python_version` so the Android `abiFilters` can
drop `armeabi-v7a` for 3.13+ (PEP 738 dropped 32-bit Android in
python-build releases).
Pyodide is no longer pre-baked into the build template. On each web
build/publish, the matching pyodide-core tarball plus the micropip /
packaging wheels are downloaded into the build output and cached under
`~/.flet/pyodide/<version>/`. `client/web/python.js` and the templated
`python.js` drop the hardcoded `defaultPyodideUrl`; `patch_index.py`
injects `flet.pyodideUrl` at build time (CDN for normal builds, local
`pyodide/pyodide.js` when `--no-cdn`).
Other:
* Bump the `serious_python` pin in the build template to 2.0.0
(multi-version support landed there as a breaking release; pin must
follow once 2.0.0 is published to pub.dev).
* Add `python_version`-aware section to the publish docs ("Choosing a
Python version"), Pyodide pairing table on the static-website page,
and a `--pre` clarification on `flet publish`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…odide
With multi-version Python, Pyodide is selected per build from
`SUPPORTED_PYTHON_VERSIONS` (currently 0.27.7 / 0.29.4 / 314.0.0a2 for
Python 3.12 / 3.13 / 3.14). The old single-line `Pyodide: 0.27.7` field
on `flet.version` was both stale and misleading once it stopped tracking
what `flet build` actually bundles.
Replace it with a multi-line listing rendered from the registry:
Flet: 0.85.3
Flutter: 3.41.7
Supported Python versions:
3.12 (Pyodide 0.27.7)
3.13 (Pyodide 0.29.4)
3.14 (Pyodide 314.0.0a2, default)
Pre-release rows render with ", pre-release" appended. `PYODIDE_VERSION`
and the `pyodide_version` export on `flet.version` are removed (the only
external consumer was the CLI version output, now updated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sort SUPPORTED_PYTHON_VERSIONS by `packaging.version.Version(r.short)` in reverse before rendering, so users see the default (newest stable) at the top instead of having to scan to the bottom of the list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`flet build` rewrites local-path dev dependencies as `<pkg> @
file://<path>` so pip installs them in place. On Windows the literal
f"file://{dev_path}" rendered `file://D:\a\...\flet`, which pip parsed
as a UNC path (`\\D:\a\...`) and failed with:
ERROR: Could not install packages due to an OSError: [Errno 2]
No such file or directory: '\\\\D:\\a\\flet\\flet\\sdk\\python\\packages\\flet'
`Path.as_uri()` produces the correct three-slash form
(`file:///D:/a/flet/...` on Windows, `file:///Users/...` on POSIX) and
URL-encodes any special characters, so it's safe on every platform.
Surfaces on flet's own apk-windows CI job
(flet-dev/flet#27111512526 job 80010220246).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each of the 14 build targets (linux/macos/windows + Android aab+apk on
all three hosts + ipa + ios-simulator + web on all three hosts) now runs
against every supported bundled CPython, producing 42 jobs total.
Matrix gains a `python_version` scalar axis and promotes `name` from an
`include` row to a base axis so GH Actions takes the cartesian product;
the existing `include` entries are kept as-is and matched by `name` to
supply per-target runner / build_cmd / artifact_path. The build step now
passes `--python-version ${{ matrix.python_version }}` to `flet build`,
and the upload step disambiguates the artifact name with a `-pyX.Y`
suffix so the 42 uploads don't collide.
`fail-fast: false` is unchanged so a single failing combo doesn't kill
the rest of the grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split the 14-target build matrix into a reusable workflow (`flet-build-test-matrix.yml`) that takes the bundled Python version as an input, and call it three times from the entrypoint — once per supported CPython release. The Actions UI now renders each call as its own collapsible "Matrix: build" card (`0/14 jobs completed`) instead of a single flat 42-row list. Adds a `python_version` choice input to `workflow_dispatch` (`all` | `3.12` | `3.13` | `3.14`) so manual runs can target a single version. push and pull_request triggers always fire all three. Each call is gated by `if: github.event_name != 'workflow_dispatch' || inputs.python_version == 'all' || inputs.python_version == 'X.Y'`. The pack job stays at the top level (its host-Python PyInstaller bundle is independent of the bundled CPython that `flet build` ships, so it doesn't need a per-Python axis). path-filter triggers now reference both workflow files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps the Dart-side `flet` package to 0.86.0 and adds the user-facing 0.86.0 entry to the top-level changelog: new `--python-version` flag, per-version Pyodide download/cache, `flet --version` listing change, the default-3.14 breaking note, removal of `flet.version.pyodide_version`, and the Windows `file://` dev-package URL fix. The Dart `flet` package gets the usual italic release-coordination note since this branch doesn't touch the Dart side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the same descending order applied to `flet --version` so users see the default (3.14) at the top of every supported-Python table instead of having to scan to the bottom of the list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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.
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>
…-version Python, native-mmap, DataChannel) (#6601) * fix(controls): preserve concrete value type when constructing ValueKey `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. * feat(transport): add dart_bridge in-process transport (alongside socket) 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. * feat(transport): export FletBackendChannel + msgpack helpers from flet.dart (lets embedders implement channelBuilder) * fix(transport): park embedded dart_bridge run loop until host shutdown 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. * templates(build): migrate from sockets to PythonBridge FFI transport 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 path for serious-python git dependency 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. * Bump 3.13.14 / 3.14.6 / Pyodide 314.0.0; thread date-based python-build 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> * feat(transport): DataChannel API for high-throughput widget byte streams 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. * feat(flet-charts): migrate MatplotlibChartCanvas to DataChannel 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. * refactor(build): split native FFI runtime out of main.dart for web compat `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. * fix(web): switch Pyodide worker to module type + pyodide.mjs 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. * docs(0.86.0): DataChannel + protocol framing breaking-change guide * 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. * perf(web): transfer Pyodide-worker bytes to main via Transferable ArrayBuffer 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. * fix(flet-charts): restore await-based backpressure for matplotlib frames 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. * ci: fix web client build after flet.version.pyodide_version removal 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> * docs(breaking-changes): drop dead /docs/extending-flet/data-channels 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. * ci: bump Node 20 actions to Node 24 versions 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`. * fix(tester): preserve ValueKey value type in find_by_key `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. * fix(tests): update example imports after folder rename in #6545 #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. * Docusaurus 3.10.1 and Node.js 24 * feat(cli): add 'flet --version --json' and source CI version/dep reads 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. * ci: pin Windows runners to windows-2025-vs2026 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. * Allow flutter_secure_storage updates (^10.0.0) 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 * Resolve Python/Pyodide versions from python-build's manifest 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. * Pin screen_brightness_macos to 2.1.2 (SPM macOS deployment-target regression) 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 * flet build: clean build dir when the bundled Python version changes 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. * Correct 0.86.0 changelog for the manifest-backed version model 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. * Bump Flutter to 3.44.2 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. * Refresh date-picker locale goldens for Flutter 3.44.2 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 * feat(app): dart_bridge session-restart loop (Android process reuse) `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. * feat(build): wire process-reuse signal + skip-on-reuse in build template 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). * feat(build): mutable _exit_port + native-log tee in python.dart bootstrap 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. * fix(build): line-buffer the native-log side of python.dart's TeeWriter 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. * Bump screen_brightness to 2.1.11 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. * fix(build/web): add pythonAlreadyRunning getter to native_runtime_stub 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. * build(android): consume serious_python native-mmap packaging + --android-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. * Move Android extract packages to android.md 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. * Update serious_python git ref to dart-bridge 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. * build: compile app + packages to .pyc by default (with --no-compile-* 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 * perf(flet): lazy public-API imports + defer eager subsystem clusters (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). * docs: version-prefix the 0.86.0 breaking-changes guides (#6599) 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. * docs(changelog): add missing 0.86.0 items 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). * docs(extend): add DataChannel section to user-extensions guide 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. * fix(build): only package requested --arch ABIs in Android APK/AAB 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> * docs(sidebar): list 0.86.0 breaking-change pages under v0.86.0 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. * Add blog post STUB: Flet v0.86.0 release announcement 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. * Pin serious_python to 3.0.0 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. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: ndonkoHenri <robotcoder4@protonmail.com>
|
@FeodorFitsner, excuse me, could you please clarify one point regarding the supported ABIs? How difficult would it be on your end to keep supporting
|
|
@bl1nch It's not difficult. We already control most of Python build process in https://github.com/flet-dev/python-build it's just >3.12 branch uses upstream CPython Android packaging suite which supports 64-bit arches only. We could just patch it and "enable" Let's look into it once the next Flet 0.86 is released? It's scope is huge. For now you can just build against Python 3.12 which supports |
* fix(controls): preserve concrete value type when constructing ValueKey
`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.
* feat(transport): add dart_bridge in-process transport (alongside socket)
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.
* feat(transport): export FletBackendChannel + msgpack helpers from flet.dart (lets embedders implement channelBuilder)
* fix(transport): park embedded dart_bridge run loop until host shutdown
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.
* templates(build): migrate from sockets to PythonBridge FFI transport
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 path for serious-python git dependency
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.
* Bump 3.13.14 / 3.14.6 / Pyodide 314.0.0; thread date-based python-build 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>
* feat(transport): DataChannel API for high-throughput widget byte streams
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.
* feat(flet-charts): migrate MatplotlibChartCanvas to DataChannel
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.
* refactor(build): split native FFI runtime out of main.dart for web compat
`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.
* fix(web): switch Pyodide worker to module type + pyodide.mjs
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.
* docs(0.86.0): DataChannel + protocol framing breaking-change guide
* 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.
* perf(web): transfer Pyodide-worker bytes to main via Transferable ArrayBuffer
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.
* fix(flet-charts): restore await-based backpressure for matplotlib frames
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.
* ci: fix web client build after flet.version.pyodide_version removal
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>
* docs(breaking-changes): drop dead /docs/extending-flet/data-channels 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.
* ci: bump Node 20 actions to Node 24 versions
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`.
* fix(tester): preserve ValueKey value type in find_by_key
`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.
* fix(tests): update example imports after folder rename in #6545
#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.
* Docusaurus 3.10.1 and Node.js 24
* feat(cli): add 'flet --version --json' and source CI version/dep reads 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.
* ci: pin Windows runners to windows-2025-vs2026
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.
* Allow flutter_secure_storage updates (^10.0.0)
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
* Resolve Python/Pyodide versions from python-build's manifest
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.
* Pin screen_brightness_macos to 2.1.2 (SPM macOS deployment-target regression)
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
* flet build: clean build dir when the bundled Python version changes
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.
* feat(testing): add `flet test` for on-device integration testing
Let Flet users write and run integration tests for their apps. Tests live in
`tests/` and drive the app running on-device (built monolithic app with embedded
Python over dart_bridge): find controls by key/text, tap, enter text, assert
state and screenshots.
- New `flet test` CLI command (mirrors `flet debug`): provisions a Flutter test
host via the build pipeline in test mode, packages the app's Python, then runs
pytest. Supports platform positional + `--device-id`, `-k`, `-u` (goldens), `-v`.
- pytest plugin shipped with `flet` (zero boilerplate): function-scoped
`flet_app` fixture (fresh app per test); also runs via plain `uv run pytest`,
with `FLET_TEST_PLATFORM`/`FLET_TEST_DEVICE`/`FLET_TEST_GOLDEN` env overrides.
- Independent tester channel: Dart `RemoteWidgetTester` <-> Python `RemoteTester`
over a raw socket with length-prefixed JSON frames, separate from Flet's
transport. The Flutter WidgetTester driver moved into `packages/flet` behind
`package:flet/testing.dart`, shared by host (`runFletHostTest`) and device
(`runFletDeviceTest`) modes.
- `flet create` scaffolds `tests/test_main.py` + pytest config; build template
gains a test_mode-gated integration_test entry point.
- Docs: getting-started/integration-testing guide + cli/flet-test reference.
* fix(testing): extract integration-test driver into flet_integration_test package
`dart pub publish --dry-run` for `packages/flet` failed: its lib/ imported the
dev-only `flutter_test`/`integration_test` packages, which pub forbids (packages
used in lib/ must be in `dependencies`). Putting the driver inside `flet` was the
wrong call — it can't ship to pub.dev that way.
Move the concrete Flutter driver (flutter_tester, flutter_test_finder,
device_test, host_test, remote_widget_tester, frame_decoder) into a new
`packages/flet_integration_test` package (publish_to: none) that depends on flet
+ integration_test. flet's published lib/ no longer references any test-only
package; the abstract Tester/TestFinder interfaces stay in flet as before.
- packages/flet: drop integration_test dev-dep, remove lib/testing.dart entry.
- packages/flet_integration_test: new package; cross-package imports of
Tester/TestFinder/Lock collapse to package:flet/flet.dart; redundant dart:io
imports dropped (flet re-exports it). Standard Flutter .gitignore.
- client + build template: import package:flet_integration_test instead of
package:flet/testing.dart; add it as a path dev-dependency (test_mode-gated in
the template).
- build_base: for local dev, rewrite the flet_integration_test path the same way
it already rewrites flet (it's publish_to:none, only resolvable from the repo).
Verified: flet `pub publish --dry-run` passes; flet_integration_test and client
integration_test analyze clean.
* fix(testing): inject test driver at build time instead of templating it
The build template referenced flet_integration_test by a repo-relative path
gated with a Jinja `{% if %}` block. That broke two things for the released
(zipped) template:
1. The release pipeline patches the template pubspec with patch_pubspec_version.py,
which does a yaml.safe_load round-trip. The uncommented `{% if %}` block made
the pubspec invalid YAML, so the patch/zip step would fail on tag.
2. A repo-relative path can't resolve once the template is zipped and shipped.
Stop putting the test dependencies in the template pubspec. flet-cli now injects
them after rendering (build_base.create_flutter_project), gated on test_mode:
- local dev: flet_integration_test by path (+ dependency_override), like flet.
- end user: flet_integration_test as a git dependency pinned to this flet
version's tag (the package is publish_to:none, never on pub.dev) — consistent
with how the template already pulls serious_python from git.
The template pubspec is now plain valid YAML again (the patch tooling round-trips
it cleanly) and a normal `flet build` never pulls the test driver.
flet_integration_test depends on flet by version (not path) with a local
dependency_override, so flet unifies to a single source across the graph whether
it's consumed by path (repo) or git (user); flutter_test becomes a regular dep so
test hosts get it transitively.
Verified: template pubspec parses; patch_pubspec_version.py round-trips it in both
release and dev modes; `flet test` provisioning injects the deps and
`flutter pub get` resolves; flet_integration_test analyzes clean.
* docs(testing): add Testing types reference + link API symbols in guide
Add a "Testing" section under Reference > Types with stubs for FletTestApp,
Tester, Finder and DisposalMode (website/docs/types/testing/), wired into the
sidebar. Replace the stale top-level mkdocs-style stubs (types/finder.md,
flettestapp.md, tester.md) that used the old `:::` syntax.
Link every API class/method/property mentioned in the integration-testing guide
to its reference page using the `[label][flet.testing.Symbol]` xref format, like
other docs.
* docs(testing): fix unresolved reST xrefs in FletTestApp docstrings
The new FletTestApp reference page surfaced two reST cross-references that didn't
resolve (caught by the docs build's reST xref check):
- `FletTestApp.tester` referenced the internal, undocumented
`flet.testing.remote_tester.RemoteTester` via :class: — changed to plain code.
- `create_gif` referenced `:meth:`Page.take_animation``; the documented symbol is
`flet.BasePage.take_animation` — corrected the target.
Verified: full docusaurus build + check_docs.sh pass (reST xrefs OK).
* ci(testing): add flet-test workflow + Counter test app
New `.github/workflows/flet-test.yml` runs `flet test` (on-device) across macOS,
iOS, Windows, Linux and Android in a single matrix, against a new
`sdk/python/examples/apps/flet_test_counter` app (keyed +/- buttons,
interaction-only test, no goldens). The embedded app is built with Python 3.14;
the host venv stays on 3.13. Per-platform handling: xvfb for Linux, a booted
simulator (UDID) for iOS, reactivecircus/android-emulator-runner (KVM) for
Android. Pins the in-repo flet packages like flet_build_test.
Allowlist UDID/udid in typos (legitimate iOS term + simctl JSON field).
Verified locally: `flet test macos --python-version 3.14` -> 1 passed.
* fix(testing): drive device-mode integration tests with benchmarkLive
After merging flet-0.86, `flet test` (device mode) hung: the very first
`WidgetTester.pump()` never returned, so the tester never connected and the run
timed out.
Root cause (isolated by single-variable bisection): the build template's #6616
`BootHost` boot structure (`runApp(StatefulWidget)` -> `initState` ->
`await prepareApp()` -> `setState`) deadlocks the default `fadePointers` frame
policy under `flutter test` — `pump()` schedules a frame and blocks on
`_pendingFrame` until it's drawn, but that frame never arrives during this boot.
Swapping only `BootHost` back to the old `runApp(FutureBuilder(...))` shape made
it pass reliably on the same Flutter 3.44.3, and the old structure on 3.44 was
fine — so it's the boot structure, not the Flutter bump or the boot screen
animation.
Fix lives entirely in the test driver (no shipping-app/template change):
- `runFletDeviceTest` sets `framePolicy = benchmarkLive` — Flutter's documented
policy "for running the test on a device". Its `pump()` doesn't wait on an
engine-drawn frame (just delays), while framework-requested frames (the app's
setState/animations, incl. Python's dart_bridge updates) still render.
- Because benchmarkLive pumps don't advance wall-clock or force frames, the
driver waits for *sustained* tree idle (the boot screen's CircularProgress
Indicator keeps it busy until Python renders the page) before connecting, and
`FlutterWidgetTester.pumpAndSettle` (gated on benchmarkLive only, so host mode
is untouched) pumps with real delays until the tree stays idle — so async
tap -> on_click -> control-update round-trips land before asserting.
Verified: counter app (+/- buttons, 0 -> 1 -> -1) passes 3/3 on macOS / Flutter
3.44.3; host-mode driver unchanged.
* fix(testing): use FutureBuilder boot path under test + propagate flutter test failures
Replaces the benchmarkLive approach (previous commit), which was wrong: it
un-blocked WidgetTester.pump() but its continuous redraw triggered a
rebuild-during-build (`!_dirty`) that *failed* the on-device flutter test — and
that failure was hidden by a false-green bug (below). Verified by comparison on
Flutter 3.44.3: BootHost+benchmarkLive => `!_dirty`, 0 passed/1 failed; the
FutureBuilder boot path => `All tests passed!`, clean, 3/3.
Two fixes:
- Template main.dart: under `FLET_TEST`, boot via the old
`runApp(FutureBuilder(future: prepareApp(), ...))` shape instead of #6616's
`BootHost`. BootHost's StatefulWidget/initState-async/setState boot deadlocks
`tester.pump()` (it blocks on a frame that never arrives during boot);
FutureBuilder is driven cleanly. Production builds are unchanged (still
BootHost). The app — embedded Python over dart_bridge, FletApp — is identical.
- FletTestApp.teardown: check the `flutter test` process exit code and raise if
non-zero. The host-side find/tap assertions can all pass while the on-device
`testWidgets` body fails (e.g. a widget exception), so pytest was reporting a
pass over a failed flutter run — a false green. Now surfaced.
Revert the benchmarkLive changes to the device driver (device_test.dart,
flutter_tester.dart) — default frame policy works with the FutureBuilder path.
Verified: counter (+/-, 0 -> 1 -> -1) genuinely passes 3/3 on macOS / 3.44.3
(`All tests passed!`, no `!_dirty`, pytest + flutter agree).
* fix(testing): use resolved Flutter exe path when spawning flutter test (Windows)
On Windows the device-mode run failed at fixture setup with
`FileNotFoundError [WinError 2]`: FletTestApp spawned `flutter test` via
`create_subprocess_exec("flutter", ...)`, but Windows' CreateProcess does no
PATHEXT lookup, so a bare "flutter" (really `flutter.bat`) isn't found.
`flet test` already resolves the real Flutter executable (full path, `.bat` on
Windows) for provisioning — pass it to the pytest subprocess as
`FLET_TEST_FLUTTER_EXE` (and propagate it via `_TEST_ENV_KEYS`), and have
FletTestApp use it as argv[0], falling back to a bare "flutter" on PATH.
* fix(testing): name the test binary after project_name (Windows/Linux desktop)
Windows and Linux `flet test` failed after a successful build with
`Unable to start executable ... Failed to find "<project_name>.exe/binary"`.
Root cause (the "doesn't build on desktop" hypothesis was wrong — Flutter does
build): the Windows/Linux runner sets the executable OUTPUT_NAME to
`artifact_name`, but `flutter test -d <desktop>` launches the binary by the
Flutter pubspec `name` (== project_name). When `[tool.flet] artifact` differs
from the project name (e.g. `flet-test-counter` vs `flet_test_counter`), the
built binary and the launched path don't match. macOS is unaffected (its `.app`
is located by the product/artifact name).
In test mode, pin `artifact_name = project_name` so the desktop binary's name
matches what the integration-test host launches. Verified: macOS still passes
(now builds `flet_test_counter.app`); fixes the Windows/Linux launch path.
* fix(test): pass serious_python native-build env to flet test
flet test spawns its own 'flutter test integration_test' (via FletTestApp)
instead of going through _run_flutter_command, so it never received the
serious_python build-time env that flet build sets. Most critically
SERIOUS_PYTHON_APP was unset, which makes the Android packageApp Gradle task
early-return and leave a stale app.zip (old-Python main.pyc) in the APK,
crashing the embedded runtime with 'ImportError: bad magic number'.
Extract the serious_python native-build env into a shared
_serious_python_build_env() and use it from both _run_flutter_command and
flet test's _flutter_path_env, so the two paths bundle an identical app and
can't drift. Adds SERIOUS_PYTHON_APP, SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES
and SP_NATIVE_SET to the test env (and _TEST_ENV_KEYS).
* ci(flet-test): capture android logcat; force software GL on linux
Android: stream device-side logs (embedded Python stdout/stderr, Flet,
Flutter, native crashes) to a file during the run and dump them in a
collapsible group afterwards, so on-device failures are diagnosable from CI.
Linux: xvfb has no GPU, so the Flutter GTK app crashes on GL context
creation (exit 79); install Mesa's software GL (llvmpipe) and force it via
LIBGL_ALWAYS_SOFTWARE/GALLIUM_DRIVER.
* ci(flet-test): non-blocking android logcat dump; linux GL diagnostics
Android: stop streaming logcat live (verbose device logs bog down the
software emulator and stall the job); instead dump the relevant slice of the
ring buffer after the run with non-blocking 'adb logcat -d'.
Linux: add a failure-diagnostic step that reports the active GL renderer
(glxinfo) and runs the built bundle directly to surface its exit-79 crash
output, which the test harness otherwise swallows.
* fix(flet-test): wait for first render in counter test; robust CI diagnostics
The counter test asserted on the first frame, but on a device the embedded
Python cold start (interpreter init + import flet + main()) can take several
seconds — longer than the device driver's fixed warmup — so find_by_text('0')
ran before the app rendered and returned 0 on the slow CI emulator (passed
locally on a faster one). pump_and_settle only settles Flutter frames, not a
pending python->dart round-trip, so poll for the first render instead.
CI: make the android logcat dump run even on failure (|| CODE=$?) with a
tight python+crash filter that won't bog the emulator; cap the linux
bundle-diagnostic with timeout so it can't hang the job.
* ci(flet-test): non-blocking android logcat; disable AT-SPI a11y on linux
Android: drop the background 'adb logcat &' (a streaming child can keep the
emulator-runner script from finishing); dump the ring buffer after the run
with non-blocking 'adb logcat -d' instead.
Linux: the app and software GL are fine (glxinfo shows llvmpipe; the bundle
runs directly without crashing) — exit 79 is specific to the integration_test
path, which enables the semantics tree and makes GTK embed an ATK a11y socket
that doesn't exist under xvfb. Disable the AT-SPI bridge (NO_AT_BRIDGE/GTK_A11Y).
* fix(flet-test): kill android false-green; poll 60s for render; logcat to artifact
The android job reported success while pytest actually failed: the
emulator-runner ran the multi-line script such that 'exit $CODE' saw an empty
CODE (and a '\'-continuation in the logcat line broke, dumping the entire
unfiltered logcat = ~58k console lines). Run the script as a single folded
line so the test command is last and its exit code is the job's, and write a
filtered device log (embedded Python + crashes only) to a file via an EXIT
trap, uploaded as an artifact instead of streaming to the console.
Also: the counter never rendered within the 10s poll window on the slow CI
emulator (cold-start embedded Python is much slower there), so poll on a 60s
deadline instead of a fixed 40 attempts.
* test(flet-test): pull serious_python from x86_64 sysconfigdata fix branch
Temporarily override serious_python_android + serious_python_platform_interface
to flet-dev/serious-python#218 (fix/android-x86_64-sysconfigdata) so the android
x86_64 CI leg validates the fix end-to-end (embedded Python no longer crashes
with ModuleNotFoundError: _sysconfigdata__android_x86_64-linux-android).
Locally confirmed: pubspec.lock resolves to the branch and stdlib.zip now ships
both aarch64 and x86_64 _sysconfigdata. Revert to the pub.dev release once #218
ships.
* ci(flet-test): add 40-min job timeout so a hung emulator auto-cancels
The android on-device run can wedge (emulator goes offline) and run until the
default 6h limit; cap the job at 40 minutes.
* fix(testing): don't hang in RemoteTester.stop() waiting on a dead client
After an on-device test passes, teardown calls RemoteTester.stop(), which did
'await self._server.wait_closed()' with no timeout. wait_closed() blocks until
the active _handle_client finishes, but _read_loop blocks on readexactly() until
EOF — and the on-device app's socket close doesn't always deliver EOF to us
(seen on Linux), so the asyncio loop hung forever after 'All tests passed!' (the
flet test process never exits). Cancel the read task so _handle_client completes,
close the writer, and bound wait_closed() with a timeout.
* fix(testing): target generated Flutter device test driver
* ci(flet-test): capture x86_64 linux integration-test verbose diagnostic
The linux job fails with 'No tests were found' + exit 79 on the x86_64 official
flutter (passes on arm64). flet_test_app already uses the file-form target and
verifies app_test.dart is non-empty, so it's neither. Re-run the integration
test directly with --verbose (unreachable dummy server) to capture which build
target/entrypoint flutter uses, whether the testWidgets body runs, and the exit
reason; upload the full verbose log as an artifact.
* fix(testing): skip linux ready-to-show wait under flet test
* fix(testing): show linux test window without ready wait
* fix(testing): size hidden linux integration test surface
* ci(flet-test): remove linux diagnostic artifact
* refactor(testing): use directory target, keep generated-driver guard
The dir->file change in 17d368b was not what fixed Linux (the window-realize
/ ready-to-show fixes were; both the dir and file forms reported 'No tests were
found' until then). Revert the flutter test target to the directory form
('integration_test') and keep only the useful part: in device mode, validate
the generated integration_test/app_test.dart exists and is non-empty so a
missing/empty driver surfaces as a clear error instead of a confusing
'No tests were found'.
* test(flet-test): simplify counter test to plain pump_and_settle + assert
Drop the _find_text_when_ready polling helper. It was a band-aid for the
android render race, but the real cause was the serious_python x86_64 crash
(PR #218) — now fixed. Try the plain template-style test and let CI confirm the
counter renders in time on the slow emulator.
* Bump Flutter SDK to 3.44.4
Update .fvmrc to pin Flutter version 3.44.4 (patch bump from 3.44.3) to ensure a consistent SDK across development and CI environments.
* ci(flet-test): drop dead mesa-utils + obsolete a11y env
- mesa-utils was only used by glxinfo in the (removed) Diagnose Linux step.
- NO_AT_BRIDGE/GTK_A11Y were added mid-debugging but didn't fix Linux (the
window realize / ready-to-show change did); the Atk-CRITICAL warnings were
non-fatal. Remove them and the now-inaccurate comment. Keep the software-GL
env (xvfb has no GPU) and the android logcat artifact (only window into
on-device failures).
* feat(cli): install Flutter on arm64 Linux via git clone; add CI leg
Flutter publishes no prebuilt arm64 Linux SDK (releases are x64-only), so
flet-cli's install_flutter downloaded a broken x64 tarball on arm64 Linux. For
arm64 Linux, clone the SDK at the version tag instead; the first 'flutter' run
then fetches the arch-appropriate engine/Dart artifacts (how fvm/git installs
work).
Add a 'linux-arm64' CI leg (ubuntu-24.04-arm) that skips the Flutter setup
action so 'flet test' installs Flutter via this path, exercising it end-to-end.
* fix(cli): precache engine after arm64 Linux Flutter clone
A bare git clone has no bin/cache, so 'dart run serious_python:main' failed
with 'could not find package sky_engine ... solving failed'. Run
'flutter precache --linux' right after the clone to populate the engine
artifacts (sky_engine + the Linux desktop engine) the prebuilt archives ship.
* test(flet-test): matrix Python 3.12/3.13/3.14; app shows + test asserts version
- CI matrix now crosses each platform with python 3.12/3.13/3.14 (job env
PYTHON_VERSION/EXPECTED_PYTHON_VERSION from matrix; dropped the workflow_dispatch
python_version input the matrix supersedes).
- Counter app displays 'Python <platform.python_version()>'.
- test_counter asserts the app reports the expected major.minor
(EXPECTED_PYTHON_VERSION), falling back to 'any version shown' locally.
Validated on macOS (renders Python 3.14.6; 1 passed).
* chore: use released serious_python 4.1.1; drop temp git override
serious_python 4.1.1 (with the x86_64 _sysconfigdata fix, PR #218) is on
pub.dev. Bump the build template serious_python 4.1.0 -> 4.1.1 and remove the
temporary git override from flet_test_counter (the fix branch was deleted after
release, which broke 'flutter pub get' on fresh runners — the linux-arm64 legs
failed with 'could not find git ref fix/android-x86_64-sysconfigdata').
* test: remove test_flet_test_app.py unit test
It imported FletTestApp, which pulls in the screenshot-comparison deps
(numpy/Pillow/scikit-image) from the optional 'test' extra at module load.
The base unit-test suite installs flet without that extra, so collection
failed with ModuleNotFoundError: No module named 'numpy'. The
__flutter_test_target device-mode guard is exercised end-to-end by the
flet-test on-device workflow.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: ndonkoHenri <robotcoder4@protonmail.com>
Description
Adds multi-version bundled CPython support to
flet buildandflet publish. Users pick which interpreter ships with their app via a new--python-versionflag (3.12 / 3.13 / 3.14) or via[project].requires-pythonin theirpyproject.toml; the default is the latest supported stable (currently 3.14). Pyodide is no longer pre-baked into the build template — the matching release for each Python version is downloaded into the build output and cached under~/.flet/pyodide/<version>/per build.Companion to flet-dev/serious-python#207 (the 2.0.0 release where the platform plugins and packager learned to parameterize the bundled Python version). The build template here pins
serious_python: 2.0.0, so this branch should land after serious_python 2.0.0 publishes to pub.dev.Highlights
flet_cli/utils/python_versions.py— single source of truth for Python ↔ CPython-standalone ↔ Pyodide ↔ wheel-platform-tag.prerelease: Truerows are opt-in (skipped in open-endedrequires-pythonresolution; reachable via explicit--python-version 3.15/==3.15.*).build_base.pyplumbing: resolves once ininitialize_command, threads--python-version+SERIOUS_PYTHON_VERSIONinto both the serious_python package step and the laterflutter build, and exposes the resolved short version to the cookiecutter template ascookiecutter.options.python_versionso the AndroidabiFilterscan droparmeabi-v7afor 3.13+ (PEP 738).flet_cli/utils/pyodide.pyfetchespyodide-core-<version>.tar.bz2+micropip/packagingwheels into a per-version cache, idempotent.patch_index.py: injectsflet.pyodideUrlper build (CDN or localpyodide/pyodide.js); the hardcodeddefaultPyodideUrlconstant is removed from bothclient/web/python.jsand the build templatepython.js. Pre-baked pyodide bundle deleted from the build template.flet --version: lists supported Python versions (newest first) instead of a single static Pyodide line — the globalflet.version.pyodide_versionexport is removed.--prevs--python-versionclarification onflet publish.flet-build-testnow runs against the full 3.12 / 3.13 / 3.14 grid (14 build targets × 3 versions = 42 jobs), grouped via a reusable workflow so the Actions UI shows three cards instead of a flat 42-row list.workflow_dispatchgains apython_versionchoice input (all/3.12/3.13/3.14) for targeted manual runs.file://URLs on Windows now use `Path.as_uri()` so paths render asfile:///D:/...instead of being misparsed as UNC.Type of change
file://URLChecklist
website/sidebars.ymlfor breaking changes, removals, and deprecations, if applicable. (Not breaking from end-user perspective; existing builds keep working with--python-version 3.12if pinned.)Additional details
Release sequencing: this branch's build template pins
serious_python: 2.0.0. Don't merge until serious_python 2.0.0 is live on pub.dev (flet-dev/serious-python#207).🤖 Generated with Claude Code
Summary by Sourcery
Add configurable bundled Python version support to Flet builds and publishes, wiring version resolution through the CLI, web runtime, templates, and CI, and switch Pyodide to a per-version, cached download instead of a baked-in bundle.
New Features:
flet buildandflet publishto select the bundled Python interpreter via a new--python-versionflag orproject.requires-python, with 3.12, 3.13, and 3.14 supported.flet --version, including which version is the default and associated Pyodide releases.Bug Fixes:
file://URLs on Windows by usingPath.as_uri()so pip resolves drive-letter paths correctly instead of treating them as UNC paths.Enhancements:
Build:
flet-build-testworkflow into a reusable matrix workflow invoked once per supported Python version, and add aworkflow_dispatchinput to target specific Python versions while preserving full matrix coverage by default.CI:
Documentation:
--preand--python-versionin the publish docs.