状态管理
State、ReactiveState、Timer 和批量更新的使用边界。
appui 的界面由状态驱动:状态变了,body() 重新执行,View 树重新生成。状态代码要短、集中、可预测。
#预期效果
示例会展示表单字段、列表选择、搜索结果、滑块和计时器如何由状态驱动刷新。
#选择哪种状态
| 用法 | 选择 | 说明 |
|---|---|---|
| 表单字段、开关、当前 tab、普通计数 | State | 最常用,写入后触发重建。 |
| 高频数值或大数组 | ReactiveState | 可走实时快路径,减少整树重建压力。 |
| 不想触发重建的对象 | Ref | 保存句柄、缓存对象、一次性资源。 |
| 列表 / 字典增删改 | ObservableList / ObservableDict | State 会自动包装 list/dict,让局部变更也能通知界面。 |
| 派生值 | computed | 从状态计算,不手动同步副本。 |
| 状态变化后的副作用 | effect | 用于日志、保存、触发外部动作。 |
普通页面先用 State。只有高频更新或大数据刷新已经明显卡顿时,再考虑 ReactiveState。
#State
State 适合绝大多数页面状态。控件读取当前字段,on_change 或按钮回调写回字段。
import appui
state = appui.State(name="", enabled=True, count=0, progress=30.0)
def set_name(value):
state.name = value
def set_enabled(value):
state.enabled = value
def set_progress(value):
state.progress = value
def add_count():
state.count += 1
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("表单", [
appui.TextField("Name", text=state.name, on_change=set_name),
appui.Toggle("Enabled", is_on=state.enabled, on_change=set_enabled),
appui.Slider(value=state.progress, minimum=0, maximum=100, on_change=set_progress),
appui.Button("Add", action=add_count).button_style("bordered_prominent"),
]),
appui.Section("当前值", [
appui.LabeledContent("name", value=state.name or "-"),
appui.LabeledContent("enabled", value=str(state.enabled)),
appui.LabeledContent("count", value=str(state.count)),
appui.LabeledContent("progress", value=f"{state.progress:.0f}%"),
]),
]).navigation_title("State")
)
appui.run(body, state=state)
多个字段一起更新时,用 batch_update:
state.batch_update(name="Demo", enabled=True, count=0)
snapshot = state.to_dict()
#ObservableList / ObservableDict
列表和字典放进 State 后会被包装成可观察容器。append、pop、update 等局部变更也能通知界面。
import appui
state = appui.State(items=["Milk", "Coffee"], selected="None")
def add_item():
state.items.append(f"Item {len(state.items) + 1}")
def item_key(item):
return item
def select_item(item):
state.selected = item
def item_row(item):
def select_current():
select_item(item)
return appui.Button(
action=select_current,
content=appui.Text(item).frame(max_width=appui.infinity, alignment="leading"),
).button_style("plain")
def body():
return appui.NavigationStack(
appui.List([
appui.Section("Items", [
appui.ForEach(state.items, row_builder=item_row, key=item_key)
]),
appui.Section("State", [
appui.LabeledContent("Selected", value=state.selected)
]),
])
.navigation_title("ObservableList")
.toolbar([
appui.ToolbarItem(
placement="navigation_bar_trailing",
content=appui.Button("Add", action=add_item),
)
])
)
appui.run(body, state=state)
如果只是偶尔改列表,也可以重新赋值:state.items = list(state.items) + ["New"]。
#Ref
Ref 保存“不属于界面事实源”的对象,写入不会触发重建。
timer_ref = appui.Ref(None)
cache_ref = appui.Ref({})
典型用途是保存 timer、网络任务、播放器句柄、滚动代理等。不要把需要显示到界面的值放进 Ref。
#computed 与 effect
computed 用来声明派生值,避免维护两份状态。
import appui
state = appui.State(query="", items=["Layout", "Controls", "Navigation"])
def set_query(value):
state.query = value
@appui.computed(state, depends_on=["query", "items"])
def visible_items():
keyword = state.query.strip().lower()
if not keyword:
return state.items
return [item for item in state.items if keyword in item.lower()]
def item_key(item):
return item
def item_row(item):
return appui.Label(item, system_image="doc.text")
def log_query_change():
print("query changed", state.query)
appui.effect(state, depends_on=["query"])(log_query_change)
def body():
rows = visible_items()
return appui.NavigationStack(
appui.List([
appui.Section(f"{len(rows)} results", [
appui.ForEach(rows, row_builder=item_row, key=item_key)
])
])
.searchable(text=state.query, on_change=set_query)
.navigation_title("computed")
)
appui.run(body, state=state)
副作用不要写进 body();body() 应该只负责返回 View。
#bind 与 ReactiveState
数值控件需要双向绑定时,可以用 appui.bind(state, "field")。
appui.Slider(**appui.bind(state, "progress"), minimum=0, maximum=100)
TextField、Toggle、Picker 的参数名不是 value,通常直接传当前值和 on_change。
ReactiveState 适合频繁更新的属性,例如 slider 值、拖动坐标、图表数据。它可以绑定实时属性通道,减少高频整树重建。
import appui
state = appui.ReactiveState(progress=30.0)
def body():
return appui.VStack([
appui.Text(f"{state.progress:.0f}%").font("largeTitle").bold(),
appui.Slider(**state.bind("progress"), minimum=0, maximum=100),
], spacing=20).padding()
appui.run(body, state=state)
普通表单不需要上 ReactiveState;只有高频更新或大数据刷新明显卡顿时再用。
#Timer 与自动刷新
Timer 要在模块级创建,初始化时传入 action。不要在 body() 里创建 Timer。
import appui
state = appui.State(seconds=0, running=False)
def tick():
state.seconds += 1
timer = appui.Timer(interval=1.0, repeats=True, action=tick)
def start():
state.running = True
timer.start()
def stop():
state.running = False
timer.stop()
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("Timer", [
appui.LabeledContent("Seconds", value=f"{state.seconds}s"),
appui.HStack([
appui.Button("Start", action=start).button_style("bordered_prominent"),
appui.Button("Stop", action=stop).button_style("bordered"),
], spacing=12),
], footer="Running" if state.running else "Stopped")
]).navigation_title("Timer")
)
appui.run(body, state=state)
auto_refresh 适合临时脚本和 demo。正式页面优先用明确的状态写入、绑定和生命周期。
#Checklist
- 需要显示到界面的值放在
State/ReactiveState。 - 列表和字典可以依赖
ObservableList/ObservableDict,也可以重新赋值。 body()里不要创建 timer、请求、文件写入。- 多字段更新用
batch_update。 - 高频数据先确认瓶颈,再换
ReactiveState。 - 行级操作使用稳定 id,不使用过滤后的下标。