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.Controllist[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 恢复。


参见

Flet中级指南
Flet高级指南
asyncio异步编程完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog