Skip to content

perf(flet): lazy public-API imports + defer eager subsystem clusters (mobile cold start)#6597

Merged
FeodorFitsner merged 4 commits into
dart-bridgefrom
flet-lazy-init
Jun 17, 2026
Merged

perf(flet): lazy public-API imports + defer eager subsystem clusters (mobile cold start)#6597
FeodorFitsner merged 4 commits into
dart-bridgefrom
flet-lazy-init

Conversation

@FeodorFitsner

@FeodorFitsner FeodorFitsner commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Cuts Python cold-start time for flet apps on mobile by importing the framework lazily instead of eagerly executing the whole ~270-module package on import flet.

Measured on a real device (moto g15, compiled .pyc build):

  • import flet: 2.02s → 0.15s (240 flet modules at import → 22).
  • PY_BOOT → first frame built: 2.10s → ~0.8s (clean, cool-device A/B).

What's here (4 commits)

  1. Lazy public API (PEP 562 __getattr__)flet/__init__.py no longer runs ~197 eager imports. All 528 public names resolve on first access via a generated _LAZY = {name: module} map and cache into globals; real imports stay under if TYPE_CHECKING: so type checkers / IDEs / __all__ are unaffected. __version__ stays eager. Resolved objects are identical to the eager imports by construction (the map is generated from the original import statements).

  2. Defer Cupertino + deprecated services off Page/View — Cupertino app bar / nav bar are annotation-only (moved to TYPE_CHECKING, annotations string-quoted); 4 deprecated page service properties construct their service via a function-local import.

  3. Defer the auth subsystem off PageAuthorization / OAuthProviderTYPE_CHECKING, TypeVar bound → string, and AuthorizationImpl default resolved lazily in Page.login().

  4. Defer components/hooks off Page/BasePage/Sessionunwrap_component imports Component lazily (cached), Renderer is function-local, and Session.schedule_effect's EffectHook is annotation-only.

Counter example import closure across 2–4: 114 → 94 modules.

Why not from __future__ import annotations

flet's control_event resolves event-handler field annotations at runtime via get_args(), which needs real type objects, not PEP 563 strings (only 2/201 control modules use future-annotations). Module-wide future-annotations breaks event resolution — confirmed by failing test_events. So the cluster deferrals string-quote only the specific non-event annotations instead.

Testing

pytest tests/: 210 passed, 9 skipped on every commit, including the introspection-sensitive suites (test_auth_lazy_imports, test_griffe_deprecations, test_from_dict, test_patch_dataclass, test_validation, test_events).

Notes

  • Commits 2–4 shrink the import graph (host-deterministic), but for an interactive app the run path reloads some clusters, so their on-device end-to-end benefit is smaller than commit 1; they help bare import flet and import-only / display-only paths. Commit 1 is the headline win.

Summary by Sourcery

Make Flet’s Python public API and several subsystems lazily imported to reduce mobile cold-start time and simplify Android packaging by relying on serious_python’s native-mmap support.

Enhancements:

  • Replace eager imports in flet.init with a PEP 562-based lazy loader that resolves and caches public API symbols on first access while preserving type-checker behavior.
  • Defer importing Cupertino controls, components/hooks, and auth-related services from Page, BasePage, View, Session, and helpers so these subsystems only load when actually used.
  • Update Android build templates and CLI to integrate serious_python’s native-mmap packaging, including support for shipping selected Python packages extracted to disk via a new android-extract-packages option and corresponding pyproject settings.
  • Switch the default serious_python dependency in the Python build template to the android-native-mmap branch and document the new Android extract_packages behavior in the publishing docs.

Build:

  • Add an --android-extract-packages CLI flag and related configuration plumbing so Android builds can mark specific Python packages to be shipped extracted rather than inside the app zip.
  • Adjust the Android app Gradle template and pubspec serious_python dependency to match the new native-mmap packaging model, eliminating the need for legacy native packaging flags.

Documentation:

  • Document Android-only extract_packages configuration and its resolution order for handling packages that require real filesystem access on mobile builds.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've reviewed this pull request using the Sourcery rules engine

`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).
…/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.
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.
…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).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying flet-website-v2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: cf0a76a
Status: ✅  Deploy successful!
Preview URL: https://0c2f00f5.flet-website-v2.pages.dev
Branch Preview URL: https://flet-lazy-init.flet-website-v2.pages.dev

View logs

@FeodorFitsner FeodorFitsner merged commit 45d6912 into dart-bridge Jun 17, 2026
70 of 101 checks passed
@FeodorFitsner FeodorFitsner deleted the flet-lazy-init branch June 17, 2026 22:30
FeodorFitsner added a commit that referenced this pull request Jun 18, 2026
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).
FeodorFitsner added a commit that referenced this pull request Jun 19, 2026
…-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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant