Flet 入门指南
最后更新:2026-03-06 Flet 是一个用纯 Python 构建跨平台 GUI 应用的框架,基于 Flutter 渲染引擎,同一份代码可以运行为: Flet 0.82 引入了全新的声明式编程模型,与旧的命令式方式有本质区别: Flet 声明式语法由三个核心概念组成,必须理解: 用 @ft.observable 装饰的 @dataclass,其字段发生变化时,所有依赖它的组件会自动重新渲染,无需任何手动通知。 用 @ft.component 装饰的函数就是一个 UI 组件。函数接收数据作为参数,返回 ft.Control 或 listft.Cont
官方文档:https://docs.flet.dev/
适用版本:Flet 0.82+(2026-05-08 核实)
最后更新:2026-03-06
本文全程使用 Flet 0.82+ 声明式语法。声明式 = 状态驱动 UI,不手动操作控件。
一、Flet 是什么
Flet 是一个用纯 Python 构建跨平台 GUI 应用的框架,基于 Flutter 渲染引擎,同一份代码可以运行为:
- 桌面应用(Windows / macOS / Linux)
- 网页应用(浏览器)
- 移动应用(iOS / Android)
声明式 vs 命令式
Flet 0.82 引入了全新的声明式编程模型,与旧的命令式方式有本质区别:
| 对比项 | 命令式(旧,0.82 之前) | 声明式(新,0.82+,推荐) |
|---|---|---|
| 核心思想 | 直接操作控件 | 管理状态,UI 自动跟随 |
| 更新方式 | self.text.value = "x" + page.update() |
set_count(count + 1) 自动更新 |
| 心智模型 | "我要改哪个控件" | "我要改什么数据" |
| 代码结构 | 类 + build() 方法 | 函数 + 装饰器 |
| 公式 | 无 | UI = f(state) |
二、安装与运行
# 安装(含所有平台支持)
pip install 'flet[all]'
# 运行为桌面应用
flet run app.py
# 运行为网页应用
flet run --web app.py
# 指定端口
flet run --web --port 8080 app.py
三、声明式三要素
Flet 声明式语法由三个核心概念组成,必须理解:
1. @ft.observable — 响应式数据
用 @ft.observable 装饰的 @dataclass,其字段发生变化时,所有依赖它的组件会自动重新渲染,无需任何手动通知。
import flet as ft
from dataclasses import dataclass, field
@ft.observable
@dataclass
class Counter:
count: int = 0
def increment(self):
self.count += 1 # 修改字段 → 自动触发 UI 更新
def decrement(self):
self.count -= 1
def reset(self):
self.count = 0
2. @ft.component — 函数式组件
用 @ft.component 装饰的函数就是一个 UI 组件。函数接收数据作为参数,返回 ft.Control 或 list[ft.Control]。
@ft.component
def CounterView(counter: Counter) -> ft.Control:
# 函数体根据当前 counter 状态构建 UI
return ft.Column([
ft.Text(f"当前计数:{counter.count}", size=24),
ft.Row([
ft.Button("-", on_click=lambda _: counter.decrement()),
ft.Button("重置", on_click=lambda _: counter.reset()),
ft.Button("+", on_click=lambda _: counter.increment()),
])
])
3. ft.use_state() — 局部状态
在组件内部管理不需要对外共享的临时状态(如输入框当前内容、折叠/展开状态等)。
value, set_value = ft.use_state(初始值)
# value:当前值
# set_value:调用后更新值并触发重渲染
四、第一个声明式应用
import flet as ft
from dataclasses import dataclass
@ft.observable
@dataclass
class Counter:
count: int = 0
def increment(self): self.count += 1
def decrement(self): self.count -= 1
@ft.component
def CounterApp() -> ft.Control:
counter, _ = ft.use_state(Counter()) # 初始化 observable 实例
return ft.Column(
controls=[
ft.Text(f"计数:{counter.count}", size=32, weight=ft.FontWeight.BOLD),
ft.Row(
controls=[
ft.ElevatedButton("- 减一", on_click=lambda _: counter.decrement()),
ft.ElevatedButton("+ 加一", on_click=lambda _: counter.increment()),
],
alignment=ft.MainAxisAlignment.CENTER,
),
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
)
def main(page: ft.Page):
page.title = "计数器"
page.render(CounterApp) # 声明式入口
ft.app(main)
五、常用控件参考
ft.Text — 文本
ft.Text(
value="Hello, Flet!",
size=16,
color=ft.Colors.BLUE,
weight=ft.FontWeight.BOLD,
italic=False,
text_align=ft.TextAlign.CENTER,
)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value |
str | "" |
显示的文本内容 |
size |
float | 14 | 字体大小 |
color |
str | None | 文字颜色 |
weight |
FontWeight | None | 字重(BOLD/W100-W900) |
italic |
bool | False | 是否斜体 |
text_align |
TextAlign | START | 对齐(START/CENTER/END/JUSTIFY) |
max_lines |
int | None | 最大行数,超出省略 |
selectable |
bool | False | 是否可选中复制 |
ft.TextField — 文本输入框
ft.TextField(
label="用户名",
hint_text="请输入用户名",
value="",
password=False,
multiline=False,
max_lines=1,
on_change=lambda e: handle_change(e.control.value),
on_submit=lambda e: handle_submit(e.control.value),
)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label |
str | None | 标签文字 |
hint_text |
str | None | 占位提示文字 |
value |
str | "" |
当前值 |
password |
bool | False | 密码模式(隐藏输入) |
multiline |
bool | False | 多行模式 |
max_lines |
int | None | 多行时最大行数 |
keyboard_type |
KeyboardType | None | 键盘类型(EMAIL/NUMBER/PHONE) |
read_only |
bool | False | 只读 |
autofocus |
bool | False | 自动获取焦点 |
on_change |
func | None | 内容变化时触发 |
on_submit |
func | None | 按回车时触发 |
ft.Button 系列
ft.ElevatedButton("主要按钮", on_click=lambda _: do_something())
ft.TextButton("文字按钮", on_click=lambda _: do_something())
ft.OutlinedButton("边框按钮", on_click=lambda _: do_something())
ft.IconButton(icon=ft.Icons.ADD, on_click=lambda _: do_something(), tooltip="添加")
ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=lambda _: do_something())
| 参数 | 类型 | 说明 |
|---|---|---|
text / 第一个位置参数 |
str | 按钮文字 |
icon |
Icons | 按钮图标 |
on_click |
func | 点击回调,参数为 ControlEvent |
disabled |
bool | 是否禁用 |
tooltip |
str | 悬浮提示文字 |
ft.Column — 垂直布局
ft.Column(
controls=[控件1, 控件2, ...],
alignment=ft.MainAxisAlignment.START,
horizontal_alignment=ft.CrossAxisAlignment.START,
spacing=10,
scroll=ft.ScrollMode.AUTO,
expand=True,
)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
controls |
list | [] |
子控件列表 |
alignment |
MainAxisAlignment | START | 主轴(垂直)对齐 |
horizontal_alignment |
CrossAxisAlignment | START | 横轴(水平)对齐 |
spacing |
float | 0 | 子控件间距(像素) |
scroll |
ScrollMode | None | 滚动模式(AUTO/ALWAYS/HIDDEN) |
expand |
bool/int | False | 是否填充剩余空间 |
MainAxisAlignment 可选值:
START END CENTER SPACE_BETWEEN SPACE_AROUND SPACE_EVENLY
ft.Row — 水平布局
参数与 ft.Column 对称,alignment 控制水平方向,vertical_alignment 控制垂直方向。
ft.Row(
controls=[控件1, 控件2, ...],
alignment=ft.MainAxisAlignment.CENTER,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
spacing=8,
wrap=True, # 超出宽度时换行
)
ft.Container — 容器(最灵活的布局控件)
ft.Container(
content=ft.Text("Hello"),
width=200,
height=100,
padding=ft.Padding(left=16, top=8, right=16, bottom=8),
margin=10,
bgcolor=ft.Colors.BLUE_100,
border_radius=12,
border=ft.border.all(1, ft.Colors.BLUE),
shadow=ft.BoxShadow(blur_radius=8, color=ft.Colors.BLACK26),
alignment=ft.alignment.center,
on_click=lambda _: print("点击了容器"),
ink=True, # 点击时显示水波纹
)
| 参数 | 类型 | 说明 |
|---|---|---|
content |
Control | 容器内的子控件 |
width / height |
float | 固定宽高 |
padding |
Padding/float | 内边距 |
margin |
Margin/float | 外边距 |
bgcolor |
str | 背景色 |
border_radius |
float/BorderRadius | 圆角 |
border |
Border | 边框 |
shadow |
BoxShadow/list | 阴影 |
alignment |
Alignment | 内容对齐 |
expand |
bool/int | 填充剩余空间 |
on_click |
func | 点击回调 |
ink |
bool | 点击波纹效果 |
ft.Checkbox 和 ft.Switch
@ft.component
def ToggleExample() -> ft.Control:
checked, set_checked = ft.use_state(False)
switched, set_switched = ft.use_state(False)
return ft.Column([
ft.Checkbox(
label="同意条款",
value=checked,
on_change=lambda e: set_checked(e.control.value),
),
ft.Switch(
label="开启通知",
value=switched,
on_change=lambda e: set_switched(e.control.value),
),
ft.Text(f"勾选:{checked},开关:{switched}"),
])
ft.Dropdown — 下拉选择
@ft.component
def DropdownExample() -> ft.Control:
selected, set_selected = ft.use_state("苹果")
return ft.Column([
ft.Dropdown(
label="选择水果",
value=selected,
options=[
ft.dropdown.Option("苹果"),
ft.dropdown.Option("香蕉"),
ft.dropdown.Option("橙子"),
],
on_change=lambda e: set_selected(e.control.value),
width=200,
),
ft.Text(f"选择了:{selected}"),
])
ft.ListView 和 ft.GridView
# ListView:垂直滚动列表
ft.ListView(
controls=[ft.Text(f"第 {i} 项") for i in range(100)],
spacing=4,
padding=8,
divider_thickness=1, # 分割线厚度
expand=True,
)
# GridView:网格布局
ft.GridView(
controls=[ft.Container(bgcolor=ft.Colors.BLUE_100, height=100) for _ in range(20)],
runs_count=3, # 每行列数
spacing=8, # 间距
run_spacing=8, # 行间距
expand=True,
)
六、事件处理
事件对象 ControlEvent
def handle_change(e: ft.ControlEvent):
print(e.control.value) # 触发事件的控件
print(e.data) # 事件数据(通常是新值的字符串形式)
print(e.page) # 当前页面对象
声明式事件处理原则
@ft.component
def FormExample() -> ft.Control:
name, set_name = ft.use_state("")
email, set_email = ft.use_state("")
submitted, set_submitted = ft.use_state(False)
def on_submit():
if name and email:
set_submitted(True)
if submitted:
return ft.Text(f"已提交:{name} / {email}", color=ft.Colors.GREEN)
return ft.Column([
ft.TextField(
label="姓名",
value=name,
on_change=lambda e: set_name(e.control.value), # 只更新状态
),
ft.TextField(
label="邮箱",
value=email,
on_change=lambda e: set_email(e.control.value), # 只更新状态
),
ft.ElevatedButton(
"提交",
on_click=lambda _: on_submit(),
disabled=not (name and email), # 状态决定是否可用
),
])
关键原则:事件处理函数只负责更新状态,不直接操作控件。
七、父子组件通信
父传子:通过参数
@ft.component
def UserCard(name: str, age: int, avatar_url: str = "") -> ft.Control:
return ft.Container(
content=ft.Row([
ft.Image(src=avatar_url, width=48, height=48) if avatar_url else ft.Icon(ft.Icons.PERSON),
ft.Column([
ft.Text(name, weight=ft.FontWeight.BOLD),
ft.Text(f"{age} 岁", size=12, color=ft.Colors.GREY),
]),
]),
padding=12,
border_radius=8,
bgcolor=ft.Colors.SURFACE_VARIANT,
)
@ft.component
def UserList() -> ft.Control:
users = [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30},
]
return ft.Column([
UserCard(name=u["name"], age=u["age"]) for u in users
])
子传父:通过回调函数
@ft.component
def AddItemForm(on_add) -> ft.Control:
"""子组件:通过 on_add 回调向父组件传递数据"""
text, set_text = ft.use_state("")
def submit():
if text.strip():
on_add(text.strip()) # 调用父组件传入的回调
set_text("") # 清空输入
return ft.Row([
ft.TextField(
value=text,
hint_text="输入内容",
on_change=lambda e: set_text(e.control.value),
on_submit=lambda _: submit(),
expand=True,
),
ft.IconButton(icon=ft.Icons.ADD, on_click=lambda _: submit()),
])
@ft.component
def ItemList() -> ft.Control:
items, set_items = ft.use_state([])
def add_item(text: str):
set_items([*items, text]) # 用新列表替换(不要 append)
return ft.Column([
AddItemForm(on_add=add_item),
ft.ListView(
controls=[ft.Text(f"• {item}") for item in items],
expand=True,
),
])
八、综合实战:待办事项应用
import flet as ft
from dataclasses import dataclass, field
@ft.observable
@dataclass
class Todo:
text: str
done: bool = False
def toggle(self):
self.done = not self.done
@ft.observable
@dataclass
class TodoApp:
todos: list[Todo] = field(default_factory=list)
def add(self, text: str):
if text.strip():
self.todos.append(Todo(text=text.strip()))
def remove(self, todo: Todo):
self.todos.remove(todo)
@property
def pending_count(self) -> int:
return sum(1 for t in self.todos if not t.done)
# 单个待办项组件
@ft.component
def TodoItem(todo: Todo, on_delete) -> ft.Control:
return ft.Row(
controls=[
ft.Checkbox(
value=todo.done,
on_change=lambda _: todo.toggle(),
),
ft.Text(
todo.text,
expand=True,
color=ft.Colors.GREY if todo.done else None,
spans=[ft.TextSpan(
style=ft.TextStyle(decoration=ft.TextDecoration.LINE_THROUGH)
)] if todo.done else [],
),
ft.IconButton(
icon=ft.Icons.DELETE_OUTLINE,
icon_color=ft.Colors.RED_300,
on_click=lambda _: on_delete(todo),
),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
)
# 添加表单组件
@ft.component
def AddTodoForm(on_add) -> ft.Control:
text, set_text = ft.use_state("")
def submit():
on_add(text)
set_text("")
return ft.Row([
ft.TextField(
value=text,
hint_text="添加新任务...",
on_change=lambda e: set_text(e.control.value),
on_submit=lambda _: submit(),
expand=True,
),
ft.ElevatedButton("添加", on_click=lambda _: submit()),
])
# 根组件
@ft.component
def TodoAppView() -> ft.Control:
app, _ = ft.use_state(TodoApp(todos=[
Todo("学习 Flet 声明式语法"),
Todo("完成第一个 Flet 项目"),
]))
return ft.Column(
controls=[
ft.Text("待办事项", size=24, weight=ft.FontWeight.BOLD),
ft.Text(
f"待完成:{app.pending_count} / {len(app.todos)} 项",
color=ft.Colors.GREY,
),
ft.Divider(),
AddTodoForm(on_add=app.add),
ft.Divider(),
ft.ListView(
controls=[
TodoItem(todo=t, on_delete=app.remove)
for t in app.todos
],
expand=True,
spacing=4,
),
],
expand=True,
spacing=8,
scroll=ft.ScrollMode.AUTO,
)
def main(page: ft.Page):
page.title = "待办事项"
page.window.width = 500
page.window.height = 600
page.padding = 20
page.render(TodoAppView)
ft.app(main)
九、常见错误与解决
1. 直接修改列表不触发更新
# 错误:直接修改列表,observable 检测不到
items.append("new item")
# 正确:用方法修改(在 @ft.observable 的 dataclass 中)
@ft.observable
@dataclass
class State:
items: list = field(default_factory=list)
def add(self, item): self.items.append(item) # 方法内修改会触发更新
2. use_state 在组件外调用
# 错误:use_state 必须在 @ft.component 函数内部调用
count, set_count = ft.use_state(0) # 不在组件内,报错
@ft.component
def MyView():
count, set_count = ft.use_state(0) # 正确
3. 事件回调中直接操作控件(命令式思维)
# 错误:命令式操作,绕过状态管理
def on_click(e):
e.control.text = "已点击" # 不要这样做
page.update() # 不要这样做
# 正确:更新状态,UI 自动跟随
def on_click(_):
set_clicked(True) # 只更新状态
4. lambda 闭包变量捕获问题
# 错误:循环中的 lambda 捕获的是变量引用,不是当前值
buttons = [ft.Button(str(i), on_click=lambda _: print(i)) for i in range(3)]
# 所有按钮都打印 2(最后一个 i 的值)
# 正确:用默认参数固定当前值
buttons = [ft.Button(str(i), on_click=lambda _, x=i: print(x)) for i in range(3)]
最佳实践
用 page.update() 而非逐控件 .update():每次调用 .update() 都会向客户端发送一次消息,批量修改多个控件后统一调用 page.update() 可减少网络往返,避免 UI 闪烁。
控件提取为函数或组件:重复出现的 UI 片段(如列表项、对话框)封装成函数返回 ft.Control,通过参数传数据,而不是在 main 函数里堆叠控件树,方便复用和测试。
状态集中在 page 或外部对象:避免在不同控件函数中维护各自的 Python 变量,将应用状态集中到一个 dataclass 实例或 page.session,通过事件回调统一修改后调用 page.update()。
on_click 绑定的 lambda 用默认参数固定循环变量:在列表推导中创建控件时,lambda _: action(i) 会捕获最后一个 i;改为 lambda _, x=i: action(x) 在创建时固定当前值。
开发时用 --hot-reload 模式:flet run --hot-reload app.py 保存即刷新,比反复启动进程更高效;Web 模式下打开浏览器开发者工具可以看到 WebSocket 通信日志辅助调试。
常见陷阱
陷阱:修改控件属性后界面没有更新
现象: 在事件回调中修改了 text_field.value = "new",但界面显示没有变化。
原因: Flet 不自动侦听属性变化,修改后必须显式通知前端。
解决: 修改属性后调用 page.update() 或 control.update();若控件在局部作用域内,确保已添加到 page.controls 后再调用更新。
陷阱:多线程/异步中更新 UI 导致竞争
现象: 在后台线程或协程中调用 page.update() 偶发报错或 UI 状态混乱。
原因: Flet 的页面对象不是线程安全的,并发修改 controls 列表可能引发竞争。
解决: 使用 page.run_task(coroutine) 将异步任务提交到 Flet 的事件循环;后台线程完成后通过 threading.Event 或队列通知主线程,在主线程事件回调中更新 UI。
陷阱:Web 模式下路由刷新后丢失页面状态
现象: 用户在浏览器刷新页面后,应用回到初始状态,之前的表单输入或导航位置丢失。
原因: Flet Web 应用每次刷新都重新建立 WebSocket 连接,main 函数重新执行,内存中的状态全部丢失。
解决: 持久化状态到 page.client_storage(localStorage)或后端数据库;使用 URL 路由参数传递关键状态,刷新时从 URL 恢复。