主题
默认情况下,Flet 应用采用命令式写法:你直接修改 visible、value 等控件属性,添加或移除控件,Flet 会替你把变更推送到页面。这对小型应用没问题,但扩展性不佳——随着界面越来越复杂,保持相关控件同步的逻辑会在各个事件处理器中重复,很容易漏改某处,最终导致界面状态不一致。
Flet 也支持声明式写法:应用的状态是唯一的事实来源,界面由状态派生而来。你不直接修改控件或其属性,而只通过给可观察字段赋值或调用钩子的 setter来改变应用状态。Flet 的组件检测到状态变化后,会重新渲染依赖该状态的界面。
本文用同一个简单的「用户管理器」应用分别演示两种写法——命令式与声明式——方便你直接对比。它是一个典型的 CRUD 应用:列出用户、新增用户、行内编辑、删除:


命令式
在这个示例中,点击按钮会直接修改控件属性(visible、value),并向页面的控件列表中添加或移除控件——这正是它「命令式」的原因。Flet 会自动把每次变更推送到页面。
- 新增(Add) 用
first_name/last_name两个TextField的值创建一个新的Item行,追加到page.controls,并清空两个输入框。 - 编辑(Edit) 把
TextField重置为该行的first_name/last_name,将text、edit_button、delete_button设为visible=False,将edit_text、save_button、cancel_button设为visible=True——把该行切换为编辑模式。 - 保存(Save) 把两个
TextField的值写回first_name/last_name和text.value,然后将text、edit_button、delete_button设为visible=True,将edit_text、save_button、cancel_button设为visible=False——以新值回到只读模式。 - 取消(Cancel) 将
text、edit_button、delete_button设为visible=True,将edit_text、save_button、cancel_button设为visible=False——不保存地回到只读模式。 - 删除(Delete) 从该行的
page.controls中移除其Item实例。
python
import flet as ft
class Item(ft.Row):
def __init__(self, first_name, last_name):
super().__init__()
self.first_name = first_name
self.last_name = last_name
self.first_name_field = ft.TextField(first_name)
self.last_name_field = ft.TextField(last_name)
self.text = ft.Text(f"{first_name} {last_name}")
self.edit_text = ft.Row(
[
self.first_name_field,
self.last_name_field,
],
visible=False,
)
self.edit_button = ft.Button("Edit", on_click=self.edit_item)
self.delete_button = ft.Button("Delete", on_click=self.delete_item)
self.save_button = ft.Button("Save", on_click=self.save_item, visible=False)
self.cancel_button = ft.Button(
"Cancel", on_click=self.cancel_item, visible=False
)
self.controls = [
self.text,
self.edit_text,
self.edit_button,
self.delete_button,
self.save_button,
self.cancel_button,
]
def delete_item(self, e):
self.page.controls.remove(self)
def edit_item(self, e):
self.first_name_field.value = self.first_name
self.last_name_field.value = self.last_name
self.text.visible = False
self.edit_button.visible = False
self.delete_button.visible = False
self.save_button.visible = True
self.cancel_button.visible = True
self.edit_text.visible = True
def save_item(self, e):
self.first_name = self.first_name_field.value
self.last_name = self.last_name_field.value
self.text.value = f"{self.first_name} {self.last_name}"
self.text.visible = True
self.edit_button.visible = True
self.delete_button.visible = True
self.save_button.visible = False
self.cancel_button.visible = False
self.edit_text.visible = False
def cancel_item(self, e):
self.text.visible = True
self.edit_button.visible = True
self.delete_button.visible = True
self.save_button.visible = False
self.cancel_button.visible = False
self.edit_text.visible = False
def main(page: ft.Page):
page.title = "CRUD Imperative Example"
def add_item(e):
item = Item(first_name.value, last_name=last_name.value)
first_name.value = ""
last_name.value = ""
page.add(item)
first_name = ft.TextField(label="First Name", width=200)
last_name = ft.TextField(label="Last Name", width=200)
page.add(
ft.Row(
[
first_name,
last_name,
ft.Button("Add", on_click=add_item),
]
)
)
if __name__ == "__main__":
ft.run(main)声明式
在这个示例中,点击按钮不会直接修改控件,而是改变与界面分离存在的应用状态。状态一旦变化,Flet 就会检测到并重新渲染界面。
要理解 Flet 如何保存状态并在其变化时检测到,你需要先了解声明式写法中的几个概念:Observables、Components 和 Hooks。
Observables
Observables (@ft.observable) are classes whose instances hold your application's state. In this "User Manager" app, each User instance holds one person's name, and a single App instance holds the list of users. The @ft.observable decorator makes their attribute and collection changes trackable: assigning to a field like user.first_name, or changing a list field like app.users.append(...), notifies the components subscribed to it, triggering them to re-render.
python
@ft.observable
@dataclass
class User:
first_name: str
last_name: str
def update(self, first_name: str, last_name: str):
self.first_name = first_name # notifies subscribed components
self.last_name = last_nameComponents
Components (@ft.component) are functions that take arguments — like user and delete_user in UserView(user, delete_user) — and return the controls describing the UI for its current state. Only the arguments that are themselves @ft.observable instances, like user, get subscribed to; a plain callback like delete_user is just passed through.
python
@ft.component
def UserView(user: User, delete_user) -> ft.Control:
return ft.Row([
ft.Text(f"{user.first_name} {user.last_name}"),
ft.Button("Delete", on_click=lambda: delete_user(user)),
])与命令式示例不同,组件不会修改已有控件的属性,也不会改动 page.controls。它在每次渲染时只是返回一组新的控件,Flet 会将其与屏幕上已有的内容做协调(reconcile),只修补发生变化的部分。
组件在初次创建时渲染一次,之后只要它所订阅的可观察对象——作为入参接收的,或通过钩子持有的——发生变化,或钩子的 setter 替换了它的值,就会再次渲染;并且只有该组件会重新渲染,而不是整个应用。每次渲染时,它都会重新订阅每一个这样的可观察对象。订阅是针对整个对象,而非个别字段——这也解释了为什么在下面的示例中,编辑某个用户只会重新渲染对应的 UserView(它订阅了那个 user),而新增或删除会重新渲染整个 AppView(它订阅了 app,而用户列表的变动就是对 app 的变动)。
Hooks
Hooks(ft.use_state)保存限定在单个组件实例范围内的局部状态——比如某一行的「编辑中」标记。普通的局部变量做不到这一点:它会在每次渲染时重置,而且修改它也不会触发渲染。调用钩子的 setter 会以与可观察对象相同的机制重新渲染该组件——只是作用范围限定在单个组件内。
python
# Broken: a plain local resets on every render, and changing it doesn't trigger one
@ft.component
def UserView(user: User, delete_user) -> ft.Control:
is_editing = False
if not is_editing:
return ft.Row([
ft.Text(f"{user.first_name} {user.last_name}"),
ft.Button("Edit", on_click=lambda: (is_editing := True)), # no re-render
])
...
# Correct: hook state survives across renders and triggers one when set
@ft.component
def UserView(user: User, delete_user) -> ft.Control:
is_editing, set_is_editing = ft.use_state(False)
if not is_editing:
return ft.Row([
ft.Text(f"{user.first_name} {user.last_name}"),
ft.Button("Edit", on_click=lambda: set_is_editing(True)),
])
...Example
状态保存在两个 @ft.observable 类的实例中:User(first_name、last_name)和 App(users: list[User])。@ft.component 函数读取这些状态并返回控件——UserView 渲染一行,可以是只读态或编辑态;AddUserForm 渲染新增表单。每一行的「编辑中」标记与输入缓冲是局部的 ft.use_state 钩子:它们仅用于显示,不属于 User。
- 新增(Add)(在
AddUserForm中)调用add_user_and_clear(),它再调用app.add_user(...)——把一个User追加到app.users——然后清空自身的局部use_state缓冲。AppView通过use_state持有app,因此这会重新渲染AppView,重建整个用户列表。 - 编辑(Edit) 调用
start_edit(),它把局部缓冲钩子重置为该用户的当前值,并调用set_is_editing(True)——只把那一行的UserView重新渲染为编辑表单。 - 保存(Save) 调用
save(),它调用user.update(...)和set_is_editing(False)——因为user是直接传入这个UserView的,所以只有这一行会以新值重新渲染。 - 取消(Cancel) 调用
cancel(),它调用set_is_editing(False)——将该行重新渲染回只读态,且不改动user。 - 删除(Delete) 调用
app.delete_user(user),把它从app.users中移除——和新增一样,这会重新渲染整个AppView,而不只是那一行。

python
from dataclasses import dataclass, field
import flet as ft
@ft.observable
@dataclass
class User:
first_name: str
last_name: str
def update(self, first_name: str, last_name: str):
self.first_name = first_name
self.last_name = last_name
@ft.observable
@dataclass
class App:
users: list[User] = field(default_factory=list)
def add_user(self, first_name: str, last_name: str):
if first_name.strip() or last_name.strip():
self.users.append(User(first_name, last_name))
def delete_user(self, user: User):
self.users.remove(user)
@ft.component
def UserView(user: User, delete_user) -> ft.Control:
# Local (transient) editing state—NOT in User
is_editing, set_is_editing = ft.use_state(False)
new_first_name, set_new_first_name = ft.use_state(user.first_name)
new_last_name, set_new_last_name = ft.use_state(user.last_name)
def start_edit():
set_new_first_name(user.first_name)
set_new_last_name(user.last_name)
set_is_editing(True)
def save():
user.update(new_first_name, new_last_name)
set_is_editing(False)
def cancel():
set_is_editing(False)
if not is_editing:
return ft.Row(
[
ft.Text(f"{user.first_name} {user.last_name}"),
ft.Button("Edit", on_click=start_edit),
ft.Button("Delete", on_click=lambda: delete_user(user)),
]
)
return ft.Row(
[
ft.TextField(
label="First Name",
value=new_first_name,
on_change=lambda e: set_new_first_name(e.control.value),
width=180,
),
ft.TextField(
label="Last Name",
value=new_last_name,
on_change=lambda e: set_new_last_name(e.control.value),
width=180,
),
ft.Button("Save", on_click=save),
ft.Button("Cancel", on_click=cancel),
]
)
@ft.component
def AddUserForm(add_user) -> ft.Control:
# Uses local buffers; calls parent action on Add
new_first_name, set_new_first_name = ft.use_state("")
new_last_name, set_new_last_name = ft.use_state("")
def add_user_and_clear():
add_user(new_first_name, new_last_name)
set_new_first_name("")
set_new_last_name("")
return ft.Row(
controls=[
ft.TextField(
label="First Name",
width=200,
value=new_first_name,
on_change=lambda e: set_new_first_name(e.control.value),
),
ft.TextField(
label="Last Name",
width=200,
value=new_last_name,
on_change=lambda e: set_new_last_name(e.control.value),
),
ft.Button("Add", on_click=add_user_and_clear),
]
)
@ft.component
def AppView() -> list[ft.Control]:
app, _ = ft.use_state(
App(
users=[
User("John", "Doe"),
User("Jane", "Doe"),
User("Foo", "Bar"),
]
)
)
return [
AddUserForm(app.add_user),
*[UserView(user, app.delete_user) for user in app.users],
]
def main(page: ft.Page):
page.render(AppView)
if __name__ == "__main__":
ft.run(main)小结
命令式和声明式不只是写同一段代码的两种方式——它们是两种思考界面的不同方式。命令式问的是「此刻屏幕上应该改什么?」——你逐步列出每次交互具体要改什么。声明式问的是「此刻我的数据长什么样?」——你把界面描述为这份数据的函数,然后让 Flet 自己去判断屏幕上发生了什么变化。
当应用的交互越来越多时,这种转变的回报最为明显:
- 调试——因为界面由状态派生,一个 bug 要么是「状态错了」,要么是「渲染错了」,而不是「某个事件处理器忘了更新某个标记」。你可以直接检查状态,而不必在一连串变更调用中追溯。
- 测试——
app.add_user(...)和user.update(...)只是普通的 Python 方法,不涉及任何 Flet 控件。你可以在测试中直接调用它们并检查返回结果,无需页面或渲染。 - 同一数据的多个视图——如果界面的两个部分都需要反映同一份状态(比如一个徽标计数、一个列表、一张图表),各自只需读取该状态即可,而不用手动保持同步。
- 复杂的多步骤界面——向导、筛选器、撤销/重做——当大量界面片段依赖相互重叠的状态时,声明式代码靠增加更多状态和组件来扩展,而不是靠增加处理器之间的相互协调。
更多声明式示例,请参见声明式示例合集——其中包括一个计数器、一个待办应用,以及像井字棋和扫雷这样的小游戏。你也可以在 Flet Studio 中交互式地试用它们。