Skip to content

默认情况下,Flet 应用采用命令式写法:你直接修改 visible、value 等控件属性,添加或移除控件,Flet 会替你把变更推送到页面。这对小型应用没问题,但扩展性不佳——随着界面越来越复杂,保持相关控件同步的逻辑会在各个事件处理器中重复,很容易漏改某处,最终导致界面状态不一致。

Flet 也支持声明式写法:应用的状态是唯一的事实来源,界面由状态派生而来。你不直接修改控件或其属性,而只通过给可观察字段赋值或调用钩子的 setter来改变应用状态。Flet 的组件检测到状态变化后,会重新渲染依赖该状态的界面。

本文用同一个简单的「用户管理器」应用分别演示两种写法——命令式与声明式——方便你直接对比。它是一个典型的 CRUD 应用:列出用户、新增用户、行内编辑、删除:

view
inline edit

命令式 ​

在这个示例中,点击按钮会直接修改控件属性(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_name

Components ​

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,而不只是那一行。
declarative data flow diagram
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 中交互式地试用它们。


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