Skip to content

Commit 336ed90

Browse files
feat: Multiprocessing support in packaged desktop apps (#6662)
* feat(build): macOS runner intercepts multiprocessing child re-execs Python's multiprocessing (spawn/forkserver start methods and the resource tracker) launches child processes by re-executing sys.executable with a CPython command line — and in a flet-built app sys.executable is the app binary itself. Each worker therefore booted a full Flutter GUI instance that treated '-B' as a dev-mode page URL, never ran the spawn payload, and never exited: one stray window per worker, hung pools, and "resource_tracker: process died unexpectedly, relaunching" loops. Add macos/Runner/main.swift as the explicit entry point (AppDelegate drops @main; the xib still instantiates and wires the delegate): before NSApplicationMain, it checks argv against dart_bridge >= 1.5.0's serious_python_is_mp_invocation and, on a match, exits with serious_python_main's return code — the embedded interpreter services the child headlessly (Py_BytesMain) with no AppKit/Flutter initialization. The child resolves the bundled stdlib/site-packages via the PYTHONHOME/PYTHONPATH the parent already setenv'd process-wide. Both entry points are resolved via dlsym from the current process image (dart_bridge is a static archive force-loaded into the host binary), so apps built against an older dart_bridge still link and launch — they just don't get the interception. Verified end-to-end on macOS with an instrumented probe app: Process+Queue round-trip with a main.py-defined worker, and a ProcessPoolExecutor run (4 workers, reused across 8 tasks) — headless children, correct results, ~4x speedup over sequential, no leftover processes. * feat(build): Windows runner intercepts multiprocessing child re-execs Same mechanism as the macOS runner: multiprocessing spawn children (and the resource tracker) re-execute sys.executable — the app .exe — with a CPython command line, which previously booted one GUI window per worker in dev-mode and never ran the spawn payload. At the top of wWinMain, before any console/COM/window/Flutter work, MaybeRunPythonChild() parses the wide command line (CommandLineToArgvW), loads dart_bridge.dll (falling back to dart_bridge_d.dll for debug-CRT builds), and resolves the wide-char entry points serious_python_is_mp_invocation_w / serious_python_main_w (dart_bridge >= 1.5.0; wide variants because decoding wWinMain argv through the ANSI code page would be lossy — serious_python_main_w wraps Py_Main). On a match, the process runs as a headless interpreter and returns its exit code. Resolution is dynamic (GetProcAddress with graceful fallthrough), so apps built against an older dart_bridge launch unchanged. Eagerly loading the DLL costs nothing on the normal startup path — the plugin loads it moments later anyway. Note: not yet runtime-verified on Windows (the macOS equivalent is verified end-to-end); needs a probe run on a Windows machine. * feat(build): Linux runner intercepts multiprocessing child re-execs Same mechanism as the macOS/Windows runners, with an extra reason to care on Linux: Python 3.14 changed the default start method from fork to forkserver (gh-84559), and the forkserver's server process is launched through the same sys.executable re-exec as spawn — so Linux apps that previously happened to work via raw fork break by default on the bundled 3.14. At the top of main(), before any GTK/Flutter initialization, maybe_run_python_child() dlopen()s libdart_bridge.so — resolved through the runner's $ORIGIN/lib RUNPATH, exactly the way the Dart FFI's DynamicLibrary.open() finds it later — and resolves serious_python_is_mp_invocation / serious_python_main (dart_bridge >= 1.5.0). On a match, the process runs as a headless interpreter and returns its exit code. Resolution is dynamic with graceful fallthrough, so apps built against an older dart_bridge launch unchanged. ${CMAKE_DL_LIBS} is added to the runner link for dlopen/dlsym (a no-op with glibc >= 2.34, where libdl is merged into libc). Note: not yet runtime-verified on Linux (the macOS equivalent is verified end-to-end); needs a probe run on a Linux machine. * fix(build): run the app module as the real __main__; point multiprocessing at the host binary Two independent multiprocessing breakages lived in the boot script, and either alone was fatal even with the runners' child interception in place: 1. __main__ identity. runpy.run_module(run_name="__main__") executes the app module in a scratch namespace that is never installed in sys.modules, so pickling any function defined in the app module failed IN THE PARENT with "Can't pickle <fn>: it's not found as __main__.<name>" — before a child was ever spawned. Replace it with _sp_run_module_as_main(): resolve the module spec (with `python -m` package semantics — pkg runs pkg.__main__ — and clear ImportErrors for loaderless/codeless specs), build a fresh module with __spec__/__file__/__cached__/__loader__/__package__ set, install it as sys.modules["__main__"], and exec the module code in its dict. Spawn children then re-import the app module as __mp_main__ via the standard init_main_from_name path — which is why the documented `if __name__ == "__main__":` guard around ft.run() is now mandatory for multiprocessing users, exactly as in plain CPython. The module is also aliased as sys.modules["__mp_main__"] in the parent, matching what multiprocessing does in children: objects defined in the app module and pickled by a child carry __module__ == "__mp_main__", and without the alias the parent fails to unpickle them (ModuleNotFoundError: __mp_main__). 2. Re-exec target. sys.executable / sys._base_executable are set to the host binary (new {host_executable} placeholder, filled from Platform.resolvedExecutable in native_runtime.dart; JSON string literals are valid Python string literals). On macOS/Windows CPython already computes the host binary itself, but on Linux bare Py_Initialize() PATH-guesses an unrelated "python3" (or none) — this makes the target the runner binary, whose argv interception services the children, on all three desktops deterministically. Set before any user import because multiprocessing snapshots sys.executable at import time. PYTHONINSPECT is also dropped from the inherited environment: it did nothing in the embedded parent, but a real interpreter child would stay open in interactive mode after its -c command completed. (Defense in depth — serious_python >= 4.3.0 stops setting it and dart_bridge's serious_python_main unsets it too.) Verified end-to-end on macOS together with the runner interception; the rendered boot script is byte-checked to compile after all placeholder substitutions. * feat(create): guard ft.run() with __main__ in the app scaffold Start every new project with the standard entry-point guard: if __name__ == "__main__": ft.run(main) With multiprocessing now working in packaged desktop apps, spawn children re-import the app's main module (as __mp_main__) exactly like plain CPython — an unguarded ft.run() would start a whole new app session in every worker process. The guard has always been Python best practice; the scaffold now models it so multiprocessing users don't learn it the hard way. Also type the scaffold's event handler parameter (e: ft.Event[ft.FloatingActionButton]) to model typed event handlers. * refactor(build): splice all dynamic boot-script values via jsonEncode JSON string/array literals are valid Python literals, so jsonEncode gives correct escaping for free. The previous hand-rolled escaping was uneven: {outLogFilename} doubled backslashes but not quotes, and {argv} escaped quotes but not backslashes — a Windows-style path in either would corrupt the generated Python source. {host_executable} already used jsonEncode; now all three share the one convention. Empty argv still renders as [""] (CPython always has a sys.argv[0]). Verified: boot script rendered through the real Dart substitution with hostile inputs (backslashes, quotes, non-ASCII) compiles as valid Python and every value round-trips exactly; full flet build macos + the multiprocessing probe suite passes unchanged. * docs: Multiprocessing cookbook recipe New cookbook page documenting multiprocessing support in Flet apps, now that packaged desktop apps service the spawn re-exec protocol: - when to reach for processes vs async/threads, and the platform/version support matrix (desktop-only, Flet >= 0.86.0; iOS/Android/browser unsupported); - the rules that are standard Python multiprocessing discipline but MANDATORY in packaged apps: the `if __name__ == "__main__":` guard (spawn/forkserver children re-import the main module), importable and picklable worker functions (top-level, plain data, no controls/page/ lambdas), and no GUI access from workers; - how it works in a `flet build` app: the embedded interpreter, the app binary recognizing CPython helper command lines and running them as a headless interpreter, and the practical consequences — sys.executable points at the app binary by design (don't set_executable() over it), freeze_support() is unnecessary-but-harmless, worker stdout isn't the app console log, and forcing fork on Linux is unsafe under a running Flutter engine; - a runnable ProcessPoolExecutor example (parallel chunk sorting with live progress) driven off the UI thread via page.run_thread. Listed in the cookbook sidebar after Subprocess. * add changelog entry * fix(build): open console.log as UTF-8 The boot script opened the console-log tee file without an explicit encoding, so Windows used the locale codepage (e.g. cp1252) and the first non-ASCII character an app printed raised UnicodeEncodeError inside the stdout tee — crashing the printing thread. macOS/Linux never hit it because their default filesystem encoding is UTF-8 already. Found while verifying the multiprocessing fix on a Windows VM: the demo app's status line contains "→" and its print() died mid-benchmark. errors="backslashreplace" additionally guarantees that even malformed data (e.g. lone surrogates) degrades to an escaped representation instead of ever breaking the log. * fix: hide the console window of the git version-fallback on Windows flet/version.py resolves the package version at import time; in a source checkout (empty baked flet_version) it falls back to `git describe`. On Windows, git.exe is a console-subsystem program, so spawning it from a windowless GUI process pops a visible conhost window. In a dev-built app this meant one console flash at app start — and, with multiprocessing now working, one more flash per spawned worker, since every spawn child re-imports flet and re-runs the fallback. Traced on a Windows VM via Win32_ProcessStartTrace: each worker pid parented a git.exe (+ conhost.exe) at click time. Pass CREATE_NO_WINDOW on Windows so the fallback stays invisible. Released packages are unaffected either way (CI bakes flet_version, so the git path never runs). * add cookbook recipe and examples * docs: add detailed usage guides to CLI command docstrings Enhance docstrings for `debug`, `test`, `create`, `build`, `run`, and `publish` CLI commands with links to detailed usage guides and examples from the Flet documentation. * docs: update README of build-template with installation, platform support, and usage guidance * breaking changes in version folders * update changelog * fix(build): splice {module_name} into the boot script via jsonEncode too The jsonEncode refactor's comment promised "every dynamic value is spliced into the boot script through jsonEncode", but {module_name} was still inserted raw inside hand-written quotes. No live bug — the module name must already survive the cookiecutter render and find_spec(), so hostile values can't reach it — but the exception made the comment wrong and left a footgun for future entry-point changes (e.g. dotted or path-like module names). Encode moduleName like the other values; the Python template now calls _sp_run_module_as_main({module_name}) without surrounding quotes, since the JSON string literal brings its own. Addresses the review note on #6662 (discussion_r3544535152). The rendered boot script is byte-checked to compile after all substitutions. * docs: drop leftover debug prints from the persistent-worker example calc_worker still carried two print() calls from local testing. They would also be misleading in a packaged app, where worker stdout isn't connected to the app's console log — which the cookbook page itself points out. * docs: fix relative links broken by the breaking-changes version folders Moving the v0.85/v0.86 breaking-change pages into v0-85-0/ and v0-86-0/ subfolders (876a006) was a pure rename, so every relative link inside them started resolving one directory too shallow and the site build failed its broken-links check ("Docusaurus found broken links!"): - "](.)" pointed at the (non-existent) version-folder index instead of the breaking-changes index -> now ../index.md - ../release-notes.md -> ../../release-notes.md - ../../{cli,reference,cookbook}/... -> ../../../{cli,reference,cookbook}/... All nine moved pages updated; every relative .md link target verified to exist, and a full local `docusaurus build` passes the broken-links check again. * Bump python build release and serious_python Update the pinned python-build release date to 20260708 and raise the build template's `serious_python` dependency to 4.3.0 to keep the Python packaging stack in sync. --------- Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
1 parent 889b5e0 commit 336ed90

37 files changed

Lines changed: 772 additions & 80 deletions

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### New features
44

5+
* Add support for **Python `multiprocessing` in packaged Flet desktop apps** built with `flet build macos`, `flet build windows`, and `flet build linux`. `multiprocessing` APIs such as `Process`, `ProcessPoolExecutor`, the `spawn`/`forkserver` start methods, and the resource tracker now work in packaged desktop apps. Previously, worker processes re-executed `sys.executable`, which pointed to the app binary itself, causing each worker to launch another GUI app instance and hang. Flet desktop runners now detect CPython multiprocessing child command lines before Flutter starts and divert them to a headless embedded Python interpreter via `dart_bridge` 1.5.0+. The Python bootstrap also runs the app module as the real `sys.modules["__main__"]` with `python -m` semantics, so top-level worker functions in `main.py` can be pickled correctly. When using `multiprocessing`, your app must follow normal Python multiprocessing rules: guard `ft.run(...)` with `if __name__ == "__main__":`, define worker functions at module top level, and do not access Flet UI objects from worker processes. See the new [Multiprocessing](https://docs.flet.dev/cookbook/multiprocessing) cookbook page. Mobile platforms remain unsupported because iOS and Android do not allow apps to spawn arbitrary child processes ([#4283](https://github.com/flet-dev/flet/issues/4283), [#6577](https://github.com/flet-dev/flet/pull/6662)) by @ndonkoHenri.
56
* Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.1), and Emscripten wheel platform tag are all resolved from `flet-dev/python-build`'s date-keyed manifest. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 4.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner.
67
* Add `ft.DataChannel`: dedicated byte channels for widgets that move bulk binary data (image frames, audio buffers, ML tensors) between Dart and Python, bypassing the MsgPack control protocol. The Dart side opens a channel via `FletBackend.of(context).openDataChannel()` and announces it to Python by firing a `data_channel_open` control event with `{channel_name, channel_id}`; the Python side declares `on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]]` and captures the channel via `self.get_data_channel(e.channel_id)`. Backed by a dedicated `PythonBridge` per channel in embedded native mode (4–7 GiB/s on M2 Pro) and by the default `ProtocolMuxedDataChannelFactory` in dev / web modes (raw-byte frames muxed over the active Flet protocol transport with a 1-byte type discriminator). Pyodide gets zero-copy outbound sends via `postMessage` Transferable ArrayBuffer. First consumer: `flet-charts` `MatplotlibChartCanvas`, migrated from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel by @FeodorFitsner.
78
* **In-process Python transport (`dart_bridge` FFI).** `package:flet` gains a third protocol transport alongside the UDS / TCP socket servers: it can run Flet's MsgPack protocol over an in-process `dart_bridge` byte channel via a `FletApp(channelBuilder: …)` seam (the `flet` package stays Python-independent — it doesn't depend on `serious_python` or know about `PythonBridge`; the embedder wires the channel). `serious_python` >= 3.0.0 uses this seam to embed the Python interpreter **in-process** instead of talking to it over a localhost socket, and the `flet build` template migrates from sockets to the FFI transport. On Android, where the OS may keep the process alive across a back-button quit and restart only the Dart VM, the transport rebinds to the new VM's `dart_bridge` ports on a session-restart signal (`libdart_bridge` >= 1.3.0) — the Python process and its in-memory state are preserved and the Flet session is rebuilt from `REGISTER_CLIENT` by @FeodorFitsner.
@@ -24,11 +25,11 @@
2425

2526
### Breaking changes
2627

27-
* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `<project>/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle) guide by @FeodorFitsner.
28+
* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `<project>/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle) guide by @FeodorFitsner.
2829
* `flet build` and `flet publish` now bundle CPython 3.14 by default (previously 3.12, implicit via the old single-version `serious_python`). Existing apps that depend on native wheels without 3.14 binaries should pin explicitly with `--python-version 3.12` (CLI), `requires-python = ">=3.12,<3.13"` (pyproject), or `SERIOUS_PYTHON_VERSION=3.12` in the build environment ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner.
2930
* Android builds now include only the ABIs the bundled Python supports, sourced per-version from python-build's manifest (`pythons.<short>.android_abis`) rather than hardcoded in flet. As of python-build `20260630`, `armeabi-v7a` (32-bit ARM) is published for **3.12, 3.13 and 3.14**, so all three build it by default; an explicit `--arch <abi>` is validated against the selected Python's supported set and fails with a clear error otherwise ([#6578](https://github.com/flet-dev/flet/pull/6578)) by @ndonkoHenri.
30-
* `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0-compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner.
31-
* Flet protocol wire format on stream-oriented transports (UDS / TCP) is incompatible with pre-0.86 servers and clients. Every packet now starts with a 4-byte little-endian length prefix and a 1-byte type discriminator (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame). WebSocket / `postMessage` / `dart_bridge` transports keep native message boundaries and only gain the type byte. The Flet CLI dev server and the in-process Python runtime are upgraded in lockstep — running `flet run` with mismatched `flet` versions across CLI and runtime is no longer supported. See the [DataChannel protocol framing upgrade](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade) guide. The `MatplotlibChartCanvas` widget transports its full / diff / clear frames via a `DataChannel` rather than `_invoke_method` arguments — visually identical, but custom code that subclassed it and overrode the apply methods may need updating by @FeodorFitsner.
31+
* `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0/compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner.
32+
* Flet protocol wire format on stream-oriented transports (UDS / TCP) is incompatible with pre-0.86 servers and clients. Every packet now starts with a 4-byte little-endian length prefix and a 1-byte type discriminator (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame). WebSocket / `postMessage` / `dart_bridge` transports keep native message boundaries and only gain the type byte. The Flet CLI dev server and the in-process Python runtime are upgraded in lockstep — running `flet run` with mismatched `flet` versions across CLI and runtime is no longer supported. See the [DataChannel protocol framing upgrade](/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade) guide. The `MatplotlibChartCanvas` widget transports its full / diff / clear frames via a `DataChannel` rather than `_invoke_method` arguments — visually identical, but custom code that subclassed it and overrode the apply methods may need updating by @FeodorFitsner.
3233

3334
### Deprecations
3435

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import multiprocessing
2+
import time
3+
4+
import flet as ft
5+
6+
7+
def long_job(seconds: int) -> None:
8+
"""Burn CPU for roughly `seconds` seconds.
9+
10+
Stands in for work you cannot interrupt from Python itself, e.g. a tight
11+
loop inside a C extension library. Runs in a worker process, which is why
12+
it can be cancelled at any point — threads can't be stopped like that.
13+
"""
14+
deadline = time.monotonic() + seconds
15+
while time.monotonic() < deadline:
16+
sum(range(1_000_000))
17+
18+
19+
def main(page: ft.Page):
20+
process = None
21+
22+
def watch(p: multiprocessing.Process):
23+
"""Wait (on a background thread) for the worker to end, then report
24+
whether it finished on its own or was cancelled."""
25+
p.join()
26+
if p.exitcode == 0:
27+
status.value = "Process finished normally."
28+
else:
29+
# terminate() ends the child with a negative exit code (-SIGTERM).
30+
status.value = f"Process was terminated (exit code {p.exitcode})."
31+
start.disabled = False
32+
cancel.disabled = True
33+
page.update()
34+
35+
def start_job():
36+
nonlocal process
37+
process = multiprocessing.Process(target=long_job, args=(30,), daemon=True)
38+
process.start()
39+
status.value = f"Running in process {process.pid}…"
40+
start.disabled = True
41+
cancel.disabled = False
42+
page.update()
43+
page.run_thread(watch, process)
44+
45+
def cancel_job():
46+
if process is not None and process.is_alive():
47+
process.terminate() # threads can't do this
48+
49+
page.add(
50+
ft.SafeArea(
51+
content=ft.Column(
52+
controls=[
53+
ft.Row(
54+
controls=[
55+
start := ft.Button("Start 30s job", on_click=start_job),
56+
cancel := ft.Button(
57+
"Cancel", on_click=cancel_job, disabled=True
58+
),
59+
]
60+
),
61+
status := ft.Text(),
62+
]
63+
)
64+
)
65+
)
66+
67+
68+
if __name__ == "__main__":
69+
ft.run(main)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import random
2+
from concurrent.futures import ProcessPoolExecutor, as_completed
3+
4+
import flet as ft
5+
6+
7+
def sort_chunk(chunk: list[float]) -> list[float]:
8+
"""Sort one chunk of data."""
9+
return sorted(chunk)
10+
11+
12+
def main(page: ft.Page):
13+
def run_sort():
14+
"""Orchestrate the pool from a background thread, updating the UI
15+
from the main process as results come in."""
16+
chunks = [[random.random() for _ in range(250_000)] for _ in range(8)]
17+
completed = 0
18+
# Without max_workers, the pool sizes itself to the machine's CPUs.
19+
with ProcessPoolExecutor() as pool:
20+
futures = [pool.submit(sort_chunk, c) for c in chunks]
21+
# as_completed() yields each future as soon as its worker is done.
22+
for _ in as_completed(futures):
23+
completed += 1
24+
progress.value = completed / len(futures)
25+
status.value = f"Sorted {completed}/{len(futures)} chunks"
26+
page.update()
27+
status.value = "Done!"
28+
page.update()
29+
30+
page.add(
31+
ft.SafeArea(
32+
content=ft.Column(
33+
controls=[
34+
ft.Button(
35+
"Start sorting", on_click=lambda: page.run_thread(run_sort)
36+
),
37+
status := ft.Text("Idle"),
38+
progress := ft.ProgressBar(value=0, width=300),
39+
]
40+
)
41+
)
42+
)
43+
44+
45+
if __name__ == "__main__":
46+
ft.run(main)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import multiprocessing
2+
import time
3+
4+
import flet as ft
5+
6+
7+
def calc_worker(conn) -> None:
8+
"""Serve calculation jobs over the pipe until told to stop.
9+
10+
Runs in a worker process that stays alive across jobs, so the process
11+
startup cost is paid once. Expensive setup (loading models, opening
12+
datasets, warming caches) could be done once here, before the loop,
13+
and reused by every job. Receiving `None` is the shutdown signal.
14+
"""
15+
while (job := conn.recv()) is not None:
16+
started = time.perf_counter()
17+
result = sum(i * i for i in range(job))
18+
conn.send((result, time.perf_counter() - started))
19+
conn.close()
20+
21+
22+
def main(page: ft.Page):
23+
# One end of the pipe goes to the worker, the other stays with the UI.
24+
parent_conn, child_conn = multiprocessing.Pipe()
25+
worker = multiprocessing.Process(
26+
target=calc_worker, args=(child_conn,), daemon=True
27+
)
28+
worker.start()
29+
30+
def submit():
31+
# One job in flight at a time: the button stays disabled until the
32+
# worker replies (a bare Pipe is not multiplexed).
33+
button.disabled = True
34+
status.value = "Working…"
35+
page.update()
36+
page.run_thread(request)
37+
38+
def request():
39+
"""Send a job to the worker and wait for its reply.
40+
41+
Should be run on a background thread: conn.recv() blocks until the worker
42+
answers, so it must stay off the UI event loop.
43+
"""
44+
parent_conn.send(100_000_000)
45+
result, duration = parent_conn.recv()
46+
status.value = (
47+
f"sum of squares = {result}\n{duration:.2f}s in process {worker.pid}"
48+
)
49+
button.disabled = False
50+
page.update()
51+
52+
page.add(
53+
ft.SafeArea(
54+
content=ft.Column(
55+
controls=[
56+
button := ft.Button("Compute in worker", on_click=submit),
57+
status := ft.Text(f"Worker ready (pid {worker.pid})"),
58+
]
59+
)
60+
)
61+
)
62+
63+
64+
if __name__ == "__main__":
65+
ft.run(main)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import multiprocessing
2+
3+
import flet as ft
4+
5+
6+
def crunch(progress_queue: multiprocessing.Queue, steps: int) -> None:
7+
"""Do `steps` slices of CPU-heavy work, reporting progress after each one.
8+
9+
Runs in a worker process, which has no access to the page — the queue is
10+
the only channel back to the UI. Values are fractions 0..1; a final `None`
11+
tells the consumer there is nothing more to read.
12+
"""
13+
for i in range(steps):
14+
sum(range(50_000_000)) # one slice of real work
15+
progress_queue.put((i + 1) / steps)
16+
progress_queue.put(None) # sentinel: no more updates
17+
18+
19+
def main(page: ft.Page):
20+
def start():
21+
button.disabled = True
22+
status.value = "Starting worker…"
23+
page.update()
24+
queue = multiprocessing.Queue()
25+
worker = multiprocessing.Process(target=crunch, args=(queue, 20), daemon=True)
26+
worker.start()
27+
page.run_thread(drain, queue, worker)
28+
29+
def drain(queue: multiprocessing.Queue, worker: multiprocessing.Process):
30+
"""Forward the worker's progress reports to the UI.
31+
32+
Runs on a background thread: queue.get() blocks until the worker
33+
reports again, so it must stay off the UI event loop.
34+
"""
35+
while (value := queue.get()) is not None:
36+
progress.value = value
37+
status.value = f"Crunching… {value:.0%}"
38+
page.update()
39+
worker.join()
40+
status.value = "Done!"
41+
button.disabled = False
42+
page.update()
43+
44+
page.add(
45+
ft.SafeArea(
46+
content=ft.Column(
47+
controls=[
48+
button := ft.Button("Start", on_click=start),
49+
progress := ft.ProgressBar(value=0, width=300),
50+
status := ft.Text(),
51+
]
52+
)
53+
)
54+
)
55+
56+
57+
if __name__ == "__main__":
58+
ft.run(main)

sdk/python/packages/flet-cli/src/flet_cli/commands/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Command(BaseBuildCommand):
1717
Android (APK/AAB), and iOS (IPA and simulator .app), with a wide range of
1818
customization options for metadata, assets, splash screens, and signing.
1919
20-
Detailed guide with usage examples: https://flet.dev/docs/publish
20+
Detailed usage guide: https://flet.dev/docs/publish
2121
"""
2222

2323
def __init__(self, parser: argparse.ArgumentParser) -> None:

0 commit comments

Comments
 (0)