appui 思维方式
从命令式 UI 切到 State 驱动的声明式 View 树。
appui 的核心规则很简单:body() 返回一棵 View 树,状态变化后重新计算这棵树。不要手动创建、保存、移动原生控件;只描述当前状态下界面应该是什么样。
#预期效果
运行示例后会看到计数、登录分支和导航路径随状态变化即时刷新,页面代码始终只描述当前 View 树。
#先记住这 4 条
| 规则 | 含义 |
|---|---|
| View 是描述 | Text、Button、VStack 只是声明界面结构。 |
| State 是事实源 | 用户输入、选择、列表数据放进 State / ReactiveState。 |
| body 要可重复执行 | body() 里不要写网络请求、文件写入、定时器创建等副作用。 |
| 修饰符有顺序 | .padding().background() 和 .background().padding() 的视觉结果不同。 |
#从命令式切到声明式
命令式 UI 常见写法是“找到控件,然后改它”。appui 的写法是“改状态,然后让界面重新声明”。
已复制
import appui
state = appui.State(count=0)
def increment():
state.count += 1
def body():
return appui.VStack([
appui.Text(f"Count: {state.count}")
.font("largeTitle")
.bold(),
appui.Button("Add", action=increment)
.button_style("bordered_prominent"),
], spacing=16).padding()
appui.run(body, state=state)
这段代码里没有“更新 label 文本”的命令。按钮只修改 state.count,下一次重建时 Text 自然显示新值。
#View 树
View 可以嵌套,也可以根据状态返回不同分支。
已复制
import appui
state = appui.State(logged_in=False)
def log_in():
state.logged_in = True
def body():
if state.logged_in:
content = appui.Text("已登录").font("title2")
else:
content = appui.Button("登录", action=log_in)
return appui.VStack([
appui.Text("欢迎").font("largeTitle").bold(),
content,
], spacing=20).padding()
appui.run(body, state=state)
条件分支应该返回完整的 View。不要在 body() 外保存某个 View 再反复修改它。
#State 先行
写界面前先列状态字段:
| 场景 | 推荐状态 |
|---|---|
| 普通字段、表单、开关 | State |
| 高频数值,如 slider、拖动、图表数据 | ReactiveState |
| 不触发重建的对象句柄 | Ref |
| 列表 / 字典增删改 | ObservableList / ObservableDict |
状态写入通常放在按钮、输入框绑定、手势或定时器回调里。多个字段一起改时用 state.batch_update(...),避免中间状态触发多次重建。
#修饰符链
修饰符返回新的 View 描述,因此可以连续调用:
已复制
(
appui.Text("Hello")
.font("title")
.foreground_color("white")
.padding()
.background("systemBlue")
.corner_radius(12)
)
常用顺序是:内容样式 -> 尺寸/间距 -> 背景/边框 -> 交互/导航。遇到视觉不对,优先检查修饰符顺序。
#导航也是数据
导航栈不要当成“打开页面”的命令集合,而是一个路径状态。
已复制
import appui
path = appui.NavigationPath()
state = appui.State()
def open_settings():
path.append({"tag": "settings"})
def go_back():
path.pop()
def settings_destination(data):
return appui.VStack([
appui.Text("设置").font("title2"),
appui.Button("返回", action=go_back),
], spacing=12).padding()
def body():
return appui.NavigationStack(
content=appui.VStack([
appui.Text("首页").navigation_title("示例"),
appui.Button("去设置页", action=open_settings),
], spacing=16).padding(),
path=path,
destinations={
"settings": settings_destination,
},
)
appui.run(body, state=state)
destinations 的回调会接收 data 参数;即使暂时不用,也要写成命名函数,避免把页面构造逻辑藏在内联回调里。
#常见误区
| 误区 | 改法 |
|---|---|
在 body() 里创建定时器或发请求 | 放到 effect、按钮回调或生命周期回调。 |
| 把 View 保存到全局变量里再修改 | 保存状态,不保存 View。 |
| 列表原地改了但界面不刷新 | 使用 ObservableList,或重新赋值一个新列表。 |
| 参数名不确定 | 以 API 参考和代码补全为准。 |
一个 body() 写成几百行 | 拆成普通 Python 函数返回子 View。 |