主题
Flet 应用运行在单一的 asyncio 事件循环上。凡是接受处理器的地方,协程都受支持,因此你可以直接使用 asyncio 和任何异步 Python 库,无需做任何封装。
这同一个循环也负责把 UI 更新发送给客户端,由此引出最重要的一条规则:
切勿阻塞事件循环
当你的处理器运行时,其他一切都不会发生 —— 不会派发任何事件,更新也无法到达屏幕。time.sleep() 或 requests.get() 这样的阻塞调用会冻结 UI,时长等于它所需的耗时。
这在所有平台都成立,不仅限于 web。
如果你是从 Flet 0.28 过来的(那时同步处理器运行在线程池上),请先阅读从 Flet 0.28 迁移到 1.0 —— 这正是发生变化的行为。
异步入门
main() 可以是 async 的,并能使用任何 asyncio API:
python
import asyncio
import flet as ft
async def main(page: ft.Page):
await asyncio.sleep(1)
page.add(ft.Text("Hello, async world!"))
ft.run(main)当你的 Flet 应用是更大应用的一部分、且由异步代码启动时,使用 await ft.run_async(main)。
控件事件处理器
处理器可以是同步或异步的,Flet 会为每个处理器选择合适的调用约定。当处理器不阻塞时,同步处理器完全没问题:
python
def handle_resize(e):
print("New page size:", page.window.width, page.window.height)
page.on_resize = handle_resize当有需要 await 的内容时,把它改为 async:
python
async def main(page: ft.Page):
async def handle_click(e):
await some_async_method()
page.add(ft.Text("Hello!"))
page.add(ft.Button(content="Say hello!", on_click=handle_click))
ft.run(main)处理器也可以完全不带参数,当你不需要事件对象时会很方便:
python
def handle_click():
counter.value += 1
ft.Button(content="Add", on_click=handle_click)异步 Lambda
Python 没有异步 lambda。对于简单的非阻塞工作,lambda 处理器没有问题:
python
page.on_error = lambda e: print("Page error:", e.data)lambda 仍然可以启动异步工作。处理器运行在事件循环上,因此总有一个正在运行的循环可以用来调度任务 —— lambda 把协程「踢出」去执行,而不是 await 它:
python
ft.Button(content="Reload", on_click=lambda e: asyncio.create_task(reload(e)))page.run_task() 通常是这个技巧更好的版本。它接收协程函数及其参数,而不是已调用的协程,并保留对任务的引用以免其在中途被垃圾回收,还会通过 Flet 的错误处理来上报异常,而不是将其丢弃:
python
ft.Button(content="Reload", on_click=lambda e: page.run_task(reload, e))处理器先于任务结束
调度一个任务会立即结束处理器,因此自动更新发生在你的协程仍在运行时,不会包含它所做的任何事情。对于任务内部做出的 UI 改动,请自行调用 update():
python
async def reload(e):
items.value = await fetch_items()
items.update() # 处理器自己的自动更新已经发生过了对于比调度一次调用更长的工作,请使用 async def 处理器。
延时等待
使用 asyncio.sleep() 而不是会阻塞循环的 time.sleep():
python
import asyncio
import flet as ft
def main(page: ft.Page):
async def handle_click(e):
await asyncio.sleep(1)
page.add(ft.Text("Hello!"))
page.add(ft.Button(content="Say hello with delay!", on_click=handle_click))
ft.run(main)通过 yield 实现中间更新
更新会在处理器结束后发送。要在中途显示进度,可把处理器写成生成器 —— 每一次 yield 都会刷出待发的更新,并给循环一个运行的机会:
python
def handle_click(e):
for i in range(10):
do_chunk(i)
progress.value = (i + 1) / 10
yield工作仍然运行在事件循环上,因此每个分块要保持短小。对于重的分块,请改为将其卸载(见下)。
后台任务
使用 page.run_task() 来启动一个生命周期长于创建它的处理器的协程。例如,一个自我刷新的倒计时控件:
python
import asyncio
import flet as ft
@ft.control
class Countdown(ft.Text):
seconds: int = 60
def did_mount(self):
self._task = self.page.run_task(self.update_timer)
def will_unmount(self):
self._task.cancel()
async def update_timer(self):
while self.seconds:
mins, secs = divmod(self.seconds, 60)
self.value = f"{mins:02d}:{secs:02d}"
self.update()
await asyncio.sleep(1)
self.seconds -= 1
def main(page: ft.Page):
page.add(Countdown(seconds=120), Countdown(seconds=60))
ft.run(main)传入协程函数,而不是已调用的协程 —— page.run_task(self.update_timer),而不是 page.run_task(self.update_timer())。否则会抛出 TypeError。
run_task() 返回一个 future,因此控件可以在 will_unmount() 中取消自己的工作,如上所示。did_mount() 和 will_unmount() 是同步的,这正是后台工作用 run_task() 启动、而不是在其中 await 的原因。
线程
有些库没有异步版本。要把阻塞调用移出循环,就在线程中运行它。
| 用途 | 时机 |
|---|---|
await asyncio.to_thread(fn, *args) | 你需要拿回结果 |
page.run_thread(fn, *args) | 即发即忘 |
page.loop.run_in_executor(pool, fn) | 你想要自己的线程池 |
asyncio.to_thread() 是通常的选择:
python
async def handle_click(e):
response = await asyncio.to_thread(requests.get, "https://api.example.com/items")
output.value = response.textpage.run_thread() 不返回结果,但它会在线程内部重新建立页面上下文,并在没有线程的 web 上内联运行其回调:
python
def handle_click(e):
page.run_thread(write_log_file, "clicked")如果你想要自己的线程池 —— 例如为了限制同时运行的操作数量 —— 可结合你控制的 executor 使用 page.loop:
python
pool = ThreadPoolExecutor(max_workers=2)
async def handle_click(e):
await page.loop.run_in_executor(pool, transcode, path)Flet 自己的共享线程池可作为 page.executor 使用。
CPU 密集型工作
线程能让 UI 保持响应,但并不会让纯 Python 工作变快:GIL 同一时刻只允许一个线程运行 Python 字节码。会释放 GIL 的 C 扩展 —— NumPy、Pillow、大多数数据库驱动 —— 在普通线程中确实能并行。
对于需要多核的纯 Python:
| 运行于 | 可用于 | |
|---|---|---|
| Subinterpreters | N 个解释器,一个进程 | 桌面、移动端、动态 web(Python 3.14+) |
| Multiprocessing | N 个进程 | 仅桌面端 |
Subinterpreters 是同样能在 iOS 和 Android 上工作的选项,因为应用无法在那里派生子进程。Multiprocessing 提供完全隔离,并允许你硬性取消一个 worker。
要从两者中的任一者上报进度,可在 future 完成时更新 UI:
python
async def handle_click(e):
done = 0
with InterpreterPoolExecutor() as pool:
futures = [page.loop.run_in_executor(pool, do_chunk, i) for i in range(100)]
for future in asyncio.as_completed(futures):
await future
done += 1
progress.value = done / 100
progress.update()Worker 函数必须定义在模块顶层。规则和完整示例见 Subinterpreters。
平台差异
| 线程 | Subinterpreters | Multiprocessing | |
|---|---|---|---|
| 桌面端 | ✅ | ✅ (3.14+) | ✅ |
| iOS、安卓 | ✅ | ✅ (3.14+) | ❌ |
| 动态网站 | ✅ | ✅ (3.14+) | ✅ |
| 静态网站 | ❌ | ❌ | ❌ |
静态网站在浏览器中用 Pyodide(一个单线程的 WebAssembly 运行时)运行你的应用。asyncio.to_thread() 在那里不可用,而 page.run_thread() 会内联运行其回调,因此无法缓解阻塞。
静态 web 应用中的长任务
由于没有任何线程可用,唯一的选项是把工作分块并在分块之间 yield、使用真正的异步库,或把工作转移到服务端。动态网站 在你的服务端作为普通 CPython 进程运行 Python,因此上面的一切都和桌面端一样适用。
PubSub 订阅者
同步的 PubSub 订阅者运行在线程池上,而不是事件循环上,因此一个阻塞的订阅者不会冻结 UI。事件处理器不按这种方式处理。