Skip to content

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.text

page.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:

运行于可用于
SubinterpretersN 个解释器,一个进程桌面、移动端、动态 web(Python 3.14+)
MultiprocessingN 个进程仅桌面端

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。

平台差异 ​

线程SubinterpretersMultiprocessing
桌面端✅✅ (3.14+)✅
iOS、安卓✅✅ (3.14+)❌
动态网站✅✅ (3.14+)✅
静态网站❌❌❌

静态网站在浏览器中用 Pyodide(一个单线程的 WebAssembly 运行时)运行你的应用。asyncio.to_thread() 在那里不可用,而 page.run_thread() 会内联运行其回调,因此无法缓解阻塞。

静态 web 应用中的长任务

由于没有任何线程可用,唯一的选项是把工作分块并在分块之间 yield、使用真正的异步库,或把工作转移到服务端。动态网站 在你的服务端作为普通 CPython 进程运行 Python,因此上面的一切都和桌面端一样适用。

PubSub 订阅者

同步的 PubSub 订阅者运行在线程池上,而不是事件循环上,因此一个阻塞的订阅者不会冻结 UI。事件处理器不按这种方式处理。


关注我,学习大模型、大数据、Python等热门技术。