> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Flet 入门指南
- URL: https://blog.vercanti.com/flet-ru-men-zhi-nan/
- Published: 2026-08-28T14:34:29.000Z
- Updated: 2026-08-28T14:56:38.000Z
- Description: 最后更新：2026-03-06 Flet 是一个用纯 Python 构建跨平台 GUI 应用的框架，基于 Flutter 渲染引擎，同一份代码可以运行为： Flet 0.82 引入了全新的声明式编程模型，与旧的命令式方式有本质区别： Flet 声明式语法由三个核心概念组成，必须理解： 用 @ft.observable 装饰的 @dataclass，其字段发生变化时，所有依赖它的组件会自动重新渲染，无需任何手动通知。 用 @ft.component 装饰的函数就是一个 UI 组件。函数接收数据作为参数，返回 ft.Control 或 listft.Cont
- Author: yellowdog
- Tags: Python, Flet

> 官方文档：<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)**          |

---

## 二、安装与运行

```bash
# 安装（含所有平台支持）
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`，其字段发生变化时，所有依赖它的组件会**自动重新渲染**，无需任何手动通知。

```python
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]`。

```python
@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()` — 局部状态

在组件内部管理不需要对外共享的临时状态（如输入框当前内容、折叠/展开状态等）。

```python
value, set_value = ft.use_state(初始值)
# value：当前值
# set_value：调用后更新值并触发重渲染

```

---

## 四、第一个声明式应用

```python
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 — 文本

```python
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 — 文本输入框

```python
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 系列

```python
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 — 垂直布局

```python
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` 控制垂直方向。

```python
ft.Row(
    controls=[控件1, 控件2, ...],
    alignment=ft.MainAxisAlignment.CENTER,
    vertical_alignment=ft.CrossAxisAlignment.CENTER,
    spacing=8,
    wrap=True,    # 超出宽度时换行
)

```

### ft.Container — 容器（最灵活的布局控件）

```python
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

```python
@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 — 下拉选择

```python
@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

```python
# 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

```python
def handle_change(e: ft.ControlEvent):
    print(e.control.value)   # 触发事件的控件
    print(e.data)            # 事件数据（通常是新值的字符串形式）
    print(e.page)            # 当前页面对象

```

### 声明式事件处理原则

```python
@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),   # 状态决定是否可用
        ),
    ])

```

**关键原则：事件处理函数只负责更新状态，不直接操作控件。**

---

## 七、父子组件通信

### 父传子：通过参数

```python
@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
    ])

```

### 子传父：通过回调函数

```python
@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,
        ),
    ])

```

---

## 八、综合实战：待办事项应用

```python
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\. 直接修改列表不触发更新

```python
# 错误：直接修改列表，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 在组件外调用

```python
# 错误：use_state 必须在 @ft.component 函数内部调用
count, set_count = ft.use_state(0)  # 不在组件内，报错

@ft.component
def MyView():
    count, set_count = ft.use_state(0)  # 正确

```

### 3\. 事件回调中直接操作控件（命令式思维）

```python
# 错误：命令式操作，绕过状态管理
def on_click(e):
    e.control.text = "已点击"   # 不要这样做
    page.update()               # 不要这样做

# 正确：更新状态，UI 自动跟随
def on_click(_):
    set_clicked(True)  # 只更新状态

```

### 4\. lambda 闭包变量捕获问题

```python
# 错误：循环中的 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 恢复。

---

## 参见

[Flet中级指南](https://blog.vercanti.com/flet-zhong-ji-zhi-nan/)  
[Flet高级指南](https://blog.vercanti.com/flet-gao-ji-zhi-nan/)  
[asyncio异步编程完全指南](https://blog.vercanti.com/asyncio-yi-bu-bian-cheng-wan-quan-zhi-nan/)