> ## 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-zhong-ji-zhi-nan/
- Published: 2026-08-28T14:34:28.000Z
- Updated: 2026-08-28T14:56:37.000Z
- Description: 最后更新：2026-03-06 继承已有控件，修改默认样式： 继承容器控件，封装复合 UI： animate 参数说明 常用 AnimationCurve： LINEAR EASE EASE_IN EASE_OUT EASE_IN_OUT BOUNCE_IN BOUNCE_OUT ELASTIC_IN ELASTIC_OUT 路由参数用 URL query string 传递，不用全局变量：通过 page.route（如 /user?id=42）传递页面间数据，而非将数据存到模块级变量。URL 方式支持浏览器历史、书签和刷新恢复，全局变量在多用户 Web
- Author: yellowdog
- Tags: Python, Flet

> 官方文档：<https://docs.flet.dev/>  
> 适用版本：Flet 0.82+（2026-05-08 核实）

最后更新：2026-03-06

> 本文全程使用 Flet 0.82+ **声明式语法**。

---

## 一、@ft.observable 深入

### 嵌套 observable

```python
import flet as ft
from dataclasses import dataclass, field

@ft.observable
@dataclass
class Address:
    city: str = ""
    country: str = ""

@ft.observable
@dataclass
class User:
    name: str = ""
    email: str = ""
    address: Address = field(default_factory=Address)

    def update_name(self, name: str):
        self.name = name

    def move_to(self, city: str, country: str):
        self.address.city = city       # 嵌套 observable 的修改也会触发更新
        self.address.country = country

@ft.component
def UserProfile(user: User) -> ft.Control:
    return ft.Column([
        ft.Text(f"姓名：{user.name}"),
        ft.Text(f"城市：{user.address.city}"),      # 嵌套字段也是响应式的
        ft.Text(f"国家：{user.address.country}"),
    ])

```

### observable 列表操作

```python
@ft.observable
@dataclass
class TaskList:
    tasks: list = field(default_factory=list)
    filter: str = "all"    # "all" | "active" | "done"

    def add(self, text: str):
        self.tasks.append({"id": len(self.tasks), "text": text, "done": False})

    def toggle(self, task_id: int):
        for t in self.tasks:
            if t["id"] == task_id:
                t["done"] = not t["done"]
        # 列表内部对象修改后需要手动通知（触发 tasks 字段的更新）
        self.tasks = list(self.tasks)   # 重新赋值触发更新

    def remove(self, task_id: int):
        self.tasks = [t for t in self.tasks if t["id"] != task_id]

    @property
    def visible_tasks(self):
        if self.filter == "active":
            return [t for t in self.tasks if not t["done"]]
        if self.filter == "done":
            return [t for t in self.tasks if t["done"]]
        return self.tasks

```

---

## 二、自定义控件

### @ft.control — 样式化控件

继承已有控件，修改默认样式：

```python
@ft.control
class PrimaryButton(ft.ElevatedButton):
    """统一主色按钮，圆角更大"""
    style: ft.ButtonStyle = field(default_factory=lambda: ft.ButtonStyle(
        shape=ft.RoundedRectangleBorder(radius=20),
        bgcolor=ft.Colors.BLUE_600,
        color=ft.Colors.WHITE,
        padding=ft.Padding(left=24, right=24, top=12, bottom=12),
    ))

@ft.control
class DangerButton(ft.ElevatedButton):
    style: ft.ButtonStyle = field(default_factory=lambda: ft.ButtonStyle(
        bgcolor=ft.Colors.RED_600,
        color=ft.Colors.WHITE,
    ))

# 使用
PrimaryButton("确认", on_click=lambda _: ...)
DangerButton("删除", on_click=lambda _: ...)

```

### @ft.control — 组合控件

继承容器控件，封装复合 UI：

```python
@ft.control
class Card(ft.Container):
    """统一卡片样式"""
    padding: ft.Padding = field(default_factory=lambda: ft.Padding(16, 16, 16, 16))
    border_radius: ft.BorderRadius = field(default_factory=lambda: ft.border_radius.all(12))
    bgcolor: str = ft.Colors.SURFACE
    shadow: ft.BoxShadow = field(default_factory=lambda: ft.BoxShadow(
        blur_radius=8,
        color=ft.Colors.BLACK12,
        offset=ft.Offset(0, 2),
    ))

@ft.control
class Avatar(ft.CircleAvatar):
    """带默认图标的头像控件"""
    radius: float = 24
    bgcolor: str = ft.Colors.BLUE_100
    color: str = ft.Colors.BLUE_600

    def build(self):
        if not self.foreground_image_url:
            self.content = ft.Icon(ft.Icons.PERSON, size=self.radius)
        return super().build()

# 使用
Card(
    content=ft.Column([
        Avatar(foreground_image_url="https://example.com/avatar.jpg"),
        ft.Text("Alice"),
    ])
)

```

---

## 三、路由与导航

### 基础路由配置

```python
import flet as ft
from dataclasses import dataclass, field

# 页面组件
@ft.component
def HomePage() -> ft.Control:
    return ft.Column([
        ft.Text("首页", size=24, weight=ft.FontWeight.BOLD),
        ft.ElevatedButton(
            "去用户列表",
            on_click=lambda e: e.page.go("/users"),
        ),
    ])

@ft.component
def UserListPage() -> ft.Control:
    users = ["Alice", "Bob", "Carol"]
    return ft.Column([
        ft.Text("用户列表", size=24),
        ft.ListView([
            ft.ListTile(
                title=ft.Text(name),
                on_click=lambda e, n=name: e.page.go(f"/users/{n}"),
            )
            for name in users
        ]),
        ft.TextButton("返回", on_click=lambda e: e.page.go("/")),
    ])

@ft.component
def UserDetailPage(name: str) -> ft.Control:
    return ft.Column([
        ft.Text(f"用户：{name}", size=24),
        ft.TextButton("返回列表", on_click=lambda e: e.page.go("/users")),
    ])

# 路由配置
def main(page: ft.Page):
    page.title = "路由示例"

    def route_change(e: ft.RouteChangeEvent):
        route = e.route

        if route == "/":
            page.render(HomePage)
        elif route == "/users":
            page.render(UserListPage)
        elif route.startswith("/users/"):
            name = route.split("/")[-1]
            page.render(lambda: UserDetailPage(name=name))
        else:
            page.render(NotFoundPage)

    page.on_route_change = route_change
    page.go(page.route or "/")

ft.app(main)

```

### 带参数的路由解析

```python
import re

def parse_route(route: str) -> tuple[str, dict]:
    """解析路由，返回 (路由名, 参数字典)"""
    patterns = [
        (r"^/$", "home", {}),
        (r"^/users$", "user_list", {}),
        (r"^/users/(\d+)$", "user_detail", lambda m: {"user_id": int(m.group(1))}),
        (r"^/posts/(\d+)/comments$", "post_comments", lambda m: {"post_id": int(m.group(1))}),
    ]
    for pattern, name, params_fn in patterns:
        m = re.match(pattern, route)
        if m:
            params = params_fn(m) if callable(params_fn) else params_fn
            return name, params
    return "not_found", {}

def main(page: ft.Page):
    def route_change(e: ft.RouteChangeEvent):
        name, params = parse_route(e.route)
        match name:
            case "home":
                page.render(HomePage)
            case "user_list":
                page.render(UserListPage)
            case "user_detail":
                page.render(lambda: UserDetailPage(**params))
            case _:
                page.render(NotFoundPage)

    page.on_route_change = route_change
    page.go(page.route or "/")

```

---

## 四、主题与样式

### 配置应用主题

```python
def main(page: ft.Page):
    page.title = "主题示例"
    page.theme_mode = ft.ThemeMode.LIGHT   # LIGHT / DARK / SYSTEM

    # 自定义主题
    page.theme = ft.Theme(
        color_scheme_seed=ft.Colors.INDIGO,   # 种子色，自动生成色系
        use_material3=True,
        font_family="Roboto",
    )

    # 暗色主题单独配置
    page.dark_theme = ft.Theme(
        color_scheme_seed=ft.Colors.INDIGO,
        use_material3=True,
    )

    page.render(MyApp)

```

### 主题色常量

```python
# Colors 常量（Material Design 色板）
ft.Colors.PRIMARY          # 主色
ft.Colors.SECONDARY        # 辅色
ft.Colors.SURFACE          # 表面色
ft.Colors.BACKGROUND       # 背景色
ft.Colors.ON_PRIMARY       # 主色上的文字色
ft.Colors.ERROR            # 错误色

# 具体颜色（带深度）
ft.Colors.BLUE             # 标准蓝
ft.Colors.BLUE_100         # 浅蓝 100
ft.Colors.BLUE_600         # 深蓝 600
ft.Colors.BLUE_ACCENT_400  # 强调蓝

# 透明度
ft.Colors.with_opacity(0.5, ft.Colors.BLACK)   # 50% 黑色

```

### 主题感知组件

```python
@ft.component
def ThemedCard(title: str, content: str) -> ft.Control:
    """自动跟随主题色的卡片"""
    return ft.Container(
        content=ft.Column([
            ft.Text(title, size=18, weight=ft.FontWeight.BOLD,
                    color=ft.Colors.PRIMARY),           # 使用主题主色
            ft.Text(content, color=ft.Colors.ON_SURFACE_VARIANT),
        ]),
        bgcolor=ft.Colors.SURFACE_VARIANT,             # 使用主题表面色
        border_radius=12,
        padding=16,
        border=ft.border.all(1, ft.Colors.OUTLINE),   # 使用主题轮廓色
    )

```

### 自定义字体

```python
def main(page: ft.Page):
    # 加载字体
    page.fonts = {
        "NotoSansSC": "fonts/NotoSansSC-Regular.ttf",
        "NotoSansSC-Bold": "fonts/NotoSansSC-Bold.ttf",
    }
    page.theme = ft.Theme(font_family="NotoSansSC")
    page.render(MyApp)

```

---

## 五、动画

### 隐式动画（属性变化自动过渡）

```python
@ft.component
def AnimatedBox() -> ft.Control:
    big, set_big = ft.use_state(False)

    size = 200 if big else 100
    color = ft.Colors.RED if big else ft.Colors.BLUE

    return ft.Column([
        ft.Container(
            width=size,
            height=size,
            bgcolor=color,
            border_radius=size / 2 if big else 8,
            animate=ft.Animation(duration=400, curve=ft.AnimationCurve.EASE_IN_OUT),
            animate_rotation=ft.Animation(300, ft.AnimationCurve.BOUNCE_OUT),
            on_click=lambda _: set_big(not big),
        ),
        ft.Text("点击切换动画", size=12, color=ft.Colors.GREY),
    ], horizontal_alignment=ft.CrossAxisAlignment.CENTER)

```

**animate 参数说明**

| 参数       | 类型             | 说明         |
| -------- | -------------- | ---------- |
| duration | int            | 动画持续时间（毫秒） |
| curve    | AnimationCurve | 缓动曲线       |

**常用 AnimationCurve：**  
`LINEAR` `EASE` `EASE_IN` `EASE_OUT` `EASE_IN_OUT` `BOUNCE_IN` `BOUNCE_OUT` `ELASTIC_IN` `ELASTIC_OUT`

### 各属性动画

```python
ft.Container(
    # ...
    animate=ft.Animation(500),           # 容器尺寸/颜色/边框动画
    animate_opacity=ft.Animation(300),   # 透明度动画
    animate_scale=ft.Animation(300),     # 缩放动画
    animate_rotation=ft.Animation(500),  # 旋转动画
    animate_offset=ft.Animation(400),    # 偏移动画（相对位置）
)

```

### AnimatedSwitcher — 切换动画

```python
@ft.component
def PageSwitcher() -> ft.Control:
    page_index, set_page_index = ft.use_state(0)

    pages = [
        ft.Text("第一页", size=32),
        ft.Text("第二页", size=32, color=ft.Colors.BLUE),
        ft.Text("第三页", size=32, color=ft.Colors.RED),
    ]

    return ft.Column([
        ft.AnimatedSwitcher(
            content=pages[page_index],
            transition=ft.AnimatedSwitcherTransition.SCALE,
            duration=300,
            reverse_duration=200,
        ),
        ft.Row([
            ft.Button(str(i+1), on_click=lambda _, i=i: set_page_index(i))
            for i in range(len(pages))
        ]),
    ])

```

---

## 六、对话框与覆盖层

### AlertDialog

```python
@ft.component
def DialogExample() -> ft.Control:
    open_dialog, set_open_dialog = ft.use_state(False)
    result, set_result = ft.use_state("")

    return ft.Column([
        ft.ElevatedButton("打开对话框", on_click=lambda _: set_open_dialog(True)),
        ft.Text(result),
        ft.AlertDialog(
            open=open_dialog,
            modal=True,
            title=ft.Text("确认操作"),
            content=ft.Text("你确定要删除这条记录吗？"),
            actions=[
                ft.TextButton("取消", on_click=lambda _: set_open_dialog(False)),
                ft.TextButton(
                    "确认删除",
                    style=ft.ButtonStyle(color=ft.Colors.RED),
                    on_click=lambda _: (set_result("已删除"), set_open_dialog(False)),
                ),
            ],
        ),
    ])

```

### BottomSheet

```python
@ft.component
def BottomSheetExample() -> ft.Control:
    open_sheet, set_open_sheet = ft.use_state(False)

    return ft.Column([
        ft.ElevatedButton("打开底部弹窗", on_click=lambda _: set_open_sheet(True)),
        ft.BottomSheet(
            open=open_sheet,
            on_dismiss=lambda _: set_open_sheet(False),
            content=ft.Container(
                content=ft.Column([
                    ft.Text("底部弹窗", size=18, weight=ft.FontWeight.BOLD),
                    ft.Divider(),
                    ft.ListTile(leading=ft.Icon(ft.Icons.SHARE), title=ft.Text("分享")),
                    ft.ListTile(leading=ft.Icon(ft.Icons.EDIT), title=ft.Text("编辑")),
                    ft.ListTile(
                        leading=ft.Icon(ft.Icons.DELETE, color=ft.Colors.RED),
                        title=ft.Text("删除", style=ft.TextStyle(color=ft.Colors.RED)),
                        on_click=lambda _: set_open_sheet(False),
                    ),
                ]),
                padding=16,
            ),
        ),
    ])

```

### SnackBar（消息提示）

```python
@ft.component
def SnackBarExample() -> ft.Control:
    show_snack, set_show_snack = ft.use_state(False)

    return ft.Column([
        ft.ElevatedButton("显示消息", on_click=lambda _: set_show_snack(True)),
        ft.SnackBar(
            open=show_snack,
            content=ft.Text("操作成功！"),
            action="撤销",
            on_action=lambda _: print("撤销"),
            on_dismiss=lambda _: set_show_snack(False),
            bgcolor=ft.Colors.GREEN_700,
        ),
    ])

```

---

## 七、导航栏与 AppBar

### AppBar

```python
def main(page: ft.Page):
    page.appbar = ft.AppBar(
        leading=ft.Icon(ft.Icons.MENU),
        leading_width=40,
        title=ft.Text("我的应用"),
        center_title=False,
        bgcolor=ft.Colors.SURFACE_VARIANT,
        actions=[
            ft.IconButton(ft.Icons.SEARCH, on_click=lambda _: ...),
            ft.IconButton(ft.Icons.NOTIFICATIONS, on_click=lambda _: ...),
            ft.PopupMenuButton(
                items=[
                    ft.PopupMenuItem("设置", icon=ft.Icons.SETTINGS),
                    ft.PopupMenuItem("帮助", icon=ft.Icons.HELP),
                    ft.PopupMenuItem(),   # 分割线
                    ft.PopupMenuItem("退出", icon=ft.Icons.EXIT_TO_APP),
                ]
            ),
        ],
    )
    page.render(MyApp)

```

### NavigationBar（底部导航）

```python
@ft.component
def AppWithNavBar() -> ft.Control:
    selected_index, set_selected_index = ft.use_state(0)

    pages = [HomePage, SearchPage, ProfilePage]
    CurrentPage = pages[selected_index]

    return ft.Column(
        controls=[
            CurrentPage(expand=True),
            ft.NavigationBar(
                selected_index=selected_index,
                on_change=lambda e: set_selected_index(e.control.selected_index),
                destinations=[
                    ft.NavigationBarDestination(icon=ft.Icons.HOME, label="首页"),
                    ft.NavigationBarDestination(icon=ft.Icons.SEARCH, label="搜索"),
                    ft.NavigationBarDestination(icon=ft.Icons.PERSON, label="我的"),
                ],
            ),
        ],
        expand=True,
        spacing=0,
    )

```

### NavigationRail（侧边导航）

```python
@ft.component
def AppWithRail() -> ft.Control:
    selected, set_selected = ft.use_state(0)
    pages = [HomePage, SettingsPage, AboutPage]
    CurrentPage = pages[selected]

    return ft.Row(
        controls=[
            ft.NavigationRail(
                selected_index=selected,
                on_change=lambda e: set_selected(e.control.selected_index),
                label_type=ft.NavigationRailLabelType.ALL,
                destinations=[
                    ft.NavigationRailDestination(icon=ft.Icons.HOME, label="首页"),
                    ft.NavigationRailDestination(icon=ft.Icons.SETTINGS, label="设置"),
                    ft.NavigationRailDestination(icon=ft.Icons.INFO, label="关于"),
                ],
            ),
            ft.VerticalDivider(width=1),
            CurrentPage(expand=True),
        ],
        expand=True,
        spacing=0,
    )

```

---

## 八、表单与验证

```python
from dataclasses import dataclass

@dataclass
class FormState:
    username: str = ""
    email: str = ""
    password: str = ""

    def validate(self) -> dict[str, str]:
        errors = {}
        if len(self.username) < 3:
            errors["username"] = "用户名至少 3 个字符"
        if "@" not in self.email:
            errors["email"] = "请输入有效的邮箱地址"
        if len(self.password) < 6:
            errors["password"] = "密码至少 6 个字符"
        return errors

@ft.component
def RegisterForm() -> ft.Control:
    form, set_form = ft.use_state(FormState())
    errors, set_errors = ft.use_state({})
    submitted, set_submitted = ft.use_state(False)

    def update_field(field: str, value: str):
        new_form = FormState(**{**form.__dict__, field: value})
        set_form(new_form)
        # 实时清除已修改字段的错误
        if field in errors:
            set_errors({k: v for k, v in errors.items() if k != field})

    def submit():
        validation_errors = form.validate()
        if validation_errors:
            set_errors(validation_errors)
        else:
            set_submitted(True)

    if submitted:
        return ft.Column([
            ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN, size=64),
            ft.Text(f"注册成功！欢迎，{form.username}", size=18),
        ], horizontal_alignment=ft.CrossAxisAlignment.CENTER)

    return ft.Column(
        controls=[
            ft.Text("注册账号", size=24, weight=ft.FontWeight.BOLD),
            ft.TextField(
                label="用户名",
                value=form.username,
                error_text=errors.get("username"),
                on_change=lambda e: update_field("username", e.control.value),
            ),
            ft.TextField(
                label="邮箱",
                value=form.email,
                error_text=errors.get("email"),
                keyboard_type=ft.KeyboardType.EMAIL,
                on_change=lambda e: update_field("email", e.control.value),
            ),
            ft.TextField(
                label="密码",
                value=form.password,
                error_text=errors.get("password"),
                password=True,
                can_reveal_password=True,
                on_change=lambda e: update_field("password", e.control.value),
            ),
            ft.ElevatedButton(
                "注册",
                width=300,
                on_click=lambda _: submit(),
            ),
        ],
        width=320,
        spacing=16,
    )

```

---

## 九、异步编程

### async 组件与异步事件

```python
import asyncio
import flet as ft

@ft.component
def AsyncDataView() -> ft.Control:
    loading, set_loading = ft.use_state(False)
    data, set_data = ft.use_state(None)
    error, set_error = ft.use_state(None)

    async def load_data():
        set_loading(True)
        set_error(None)
        try:
            await asyncio.sleep(1.5)   # 模拟网络请求
            set_data({"name": "Alice", "score": 98})
        except Exception as e:
            set_error(str(e))
        finally:
            set_loading(False)

    if loading:
        return ft.Column([
            ft.ProgressRing(),
            ft.Text("加载中...", color=ft.Colors.GREY),
        ], horizontal_alignment=ft.CrossAxisAlignment.CENTER)

    if error:
        return ft.Column([
            ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED, size=48),
            ft.Text(f"错误：{error}", color=ft.Colors.RED),
            ft.ElevatedButton("重试", on_click=lambda _: asyncio.create_task(load_data())),
        ])

    if data is None:
        return ft.ElevatedButton("加载数据", on_click=lambda _: asyncio.create_task(load_data()))

    return ft.Column([
        ft.Text(f"姓名：{data['name']}"),
        ft.Text(f"分数：{data['score']}"),
        ft.TextButton("刷新", on_click=lambda _: asyncio.create_task(load_data())),
    ])

```

### 与 httpx 集成（真实网络请求）

```python
import httpx
import asyncio
import flet as ft
from dataclasses import dataclass, field

@ft.observable
@dataclass
class PostsState:
    posts: list = field(default_factory=list)
    loading: bool = False
    error: str = ""

    async def fetch(self):
        self.loading = True
        self.error = ""
        try:
            async with httpx.AsyncClient() as client:
                resp = await client.get("https://jsonplaceholder.typicode.com/posts?_limit=10")
                resp.raise_for_status()
                self.posts = resp.json()
        except httpx.HTTPError as e:
            self.error = f"网络错误：{e}"
        finally:
            self.loading = False

@ft.component
def PostsList() -> ft.Control:
    state, _ = ft.use_state(PostsState())

    if state.loading:
        return ft.ProgressRing()

    if state.error:
        return ft.Column([
            ft.Text(state.error, color=ft.Colors.RED),
            ft.ElevatedButton("重试", on_click=lambda _: asyncio.create_task(state.fetch())),
        ])

    if not state.posts:
        return ft.ElevatedButton(
            "加载文章",
            on_click=lambda _: asyncio.create_task(state.fetch()),
        )

    return ft.Column([
        ft.Text("文章列表", size=20, weight=ft.FontWeight.BOLD),
        ft.ListView(
            controls=[
                ft.ListTile(
                    title=ft.Text(p["title"][:40]),
                    subtitle=ft.Text(p["body"][:60]),
                )
                for p in state.posts
            ],
            spacing=4,
            expand=True,
        ),
    ], expand=True)

```

---

## 十、客户端存储

```python
import flet as ft
from dataclasses import dataclass
import json

@ft.observable
@dataclass
class Settings:
    theme: str = "light"
    language: str = "zh"
    notifications: bool = True

@ft.component
def SettingsView() -> ft.Control:
    settings, set_settings = ft.use_state(Settings())
    saved, set_saved = ft.use_state(False)

    async def load_settings(page: ft.Page):
        """从本地存储加载设置"""
        stored = await page.client_storage.get_async("app_settings")
        if stored:
            data = json.loads(stored)
            set_settings(Settings(**data))

    async def save_settings(page: ft.Page):
        """保存设置到本地存储"""
        await page.client_storage.set_async(
            "app_settings",
            json.dumps(settings.__dict__)
        )
        set_saved(True)

    def update_settings(**kwargs):
        new = Settings(**{**settings.__dict__, **kwargs})
        set_settings(new)
        set_saved(False)

    return ft.Column([
        ft.Text("应用设置", size=20, weight=ft.FontWeight.BOLD),
        ft.Dropdown(
            label="主题",
            value=settings.theme,
            options=[
                ft.dropdown.Option("light", "浅色"),
                ft.dropdown.Option("dark", "深色"),
                ft.dropdown.Option("system", "跟随系统"),
            ],
            on_change=lambda e: update_settings(theme=e.control.value),
        ),
        ft.Switch(
            label="开启通知",
            value=settings.notifications,
            on_change=lambda e: update_settings(notifications=e.control.value),
        ),
        ft.ElevatedButton(
            "保存设置",
            on_click=lambda e: asyncio.create_task(save_settings(e.page)),
        ),
        ft.Text("已保存!", color=ft.Colors.GREEN) if saved else ft.Container(),
    ], spacing=16)

```

---

## 十一、拖放（Drag & Drop）

```python
import flet as ft
from dataclasses import dataclass, field

@ft.observable
@dataclass
class KanbanState:
    todo: list = field(default_factory=lambda: ["任务A", "任务B", "任务C"])
    doing: list = field(default_factory=lambda: ["任务D"])
    done: list = field(default_factory=list)

    def move(self, item: str, from_col: str, to_col: str):
        getattr(self, from_col).remove(item)
        getattr(self, to_col).append(item)
        # 触发更新
        self.todo = list(self.todo)
        self.doing = list(self.doing)
        self.done = list(self.done)

@ft.component
def KanbanColumn(title: str, items: list, col_name: str, state: KanbanState) -> ft.Control:
    return ft.Container(
        content=ft.Column([
            ft.Text(title, weight=ft.FontWeight.BOLD),
            ft.DragTarget(
                group="kanban",
                content=ft.Column(
                    controls=[
                        ft.Draggable(
                            group="kanban",
                            data=f"{col_name}|{item}",
                            content=ft.Container(
                                content=ft.Text(item),
                                bgcolor=ft.Colors.SURFACE,
                                padding=8,
                                border_radius=4,
                                border=ft.border.all(1, ft.Colors.OUTLINE),
                            ),
                        )
                        for item in items
                    ],
                    spacing=4,
                    height=200,
                ),
                on_accept=lambda e: state.move(
                    *e.data.split("|"),
                    col_name,
                ),
            ),
        ]),
        bgcolor=ft.Colors.SURFACE_VARIANT,
        padding=12,
        border_radius=8,
        width=180,
    )

@ft.component
def KanbanBoard() -> ft.Control:
    state, _ = ft.use_state(KanbanState())

    return ft.Row([
        KanbanColumn("待处理", state.todo, "todo", state),
        KanbanColumn("进行中", state.doing, "doing", state),
        KanbanColumn("已完成", state.done, "done", state),
    ], spacing=12)

```

---

## 最佳实践

**路由参数用 URL query string 传递，不用全局变量**：通过 `page.route`（如 `/user?id=42`）传递页面间数据，而非将数据存到模块级变量。URL 方式支持浏览器历史、书签和刷新恢复，全局变量在多用户 Web 部署时会产生数据混用。

**主题统一在 `page.theme` 配置，不要在控件上硬编码颜色**：用 `ft.Theme(color_scheme_seed=ft.Colors.BLUE)` 和语义颜色（`ft.Colors.PRIMARY`、`ft.Colors.ON_SURFACE`），使暗色模式切换自动生效，避免手动维护两套颜色值。

**动画使用 `AnimatedSwitcher` 而非手动控制 `visible`**：切换控件显隐时，`ft.AnimatedSwitcher` 自动处理进出场动画，且无需管理 `page.update()` 时机；手动切换 `visible` 没有过渡效果且更难维护。

**表单验证在 `on_submit` 中统一处理**：不要在每个字段的 `on_change` 里实时校验所有规则（性能差），而是在提交时做完整校验，并在 `TextField.error_text` 上反馈错误信息，校验通过后清空 `error_text`。

**拖拽列表用 `key` 参数保证控件身份**：`ft.Draggable` 列表在重排后，Flet 通过 `key` 匹配控件身份以计算最小 diff；不设置 `key` 时可能出现控件状态错位（如输入框内容跑到相邻控件中）。

---

## 常见陷阱

### 陷阱：路由切换后旧页面的事件回调仍然触发

**现象：** 导航到新页面后，旧页面的定时器或后台任务仍在运行并调用 `page.update()`，导致 UI 异常或报错。  
**原因：** Flet 不会自动取消旧视图的异步任务；路由切换只是替换 `page.views`，旧回调仍持有对 `page` 的引用。  
**解决：** 在 `on_view_pop` 或路由变化回调中显式取消后台任务（`task.cancel()`）；使用 `page.on_disconnect` 钩子做清理。

### 陷阱：暗色模式下自定义颜色与主题不协调

**现象：** 切换到暗色模式后，某些区域颜色与背景反差不足或过于突兀。  
**原因：** 控件上直接写了 `bgcolor=ft.Colors.WHITE` 或 `color="#333333"` 等固定颜色值，暗色模式不会自动反转。  
**解决：** 统一使用主题语义颜色（`ft.Colors.SURFACE`、`ft.Colors.ON_PRIMARY` 等），或通过 `page.theme_mode` 变化时动态更新颜色。

### 陷阱：`Dropdown` 的 `value` 与 `options` 中的值不匹配导致显示空白

**现象：** `Dropdown` 设置了 `value="active"`，但下拉框显示为空。  
**原因：** `options` 中的每个 `ft.dropdown.Option` 的 `key` 必须与 `value` 精确匹配（区分大小写和类型），若 `key` 是 `"Active"`（首字母大写），则 `value="active"` 无法匹配。  
**解决：** 确保 `value` 的值与 `Option(key=...)` 的 `key` 完全一致；建议统一用枚举或常量管理选项值，避免字符串拼写错误。

---

## 参见

[Flet入门指南](https://blog.vercanti.com/flet-ru-men-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/)