性能与实时界面
普通 State、Timer、ReactiveState 和实时快路径的选择。
appui 的性能优化优先从界面结构、状态粒度和后台任务开始。大多数页面不需要特殊技巧,只要避免在 body() 里做重活,就能保持流畅。如果页面有高频属性更新,再看 实时快路径。
#预期效果
示例会展示列表筛选、状态粒度、Timer 和高频属性更新的性能写法。
#优先优化的地方
| 场景 | 推荐做法 |
|---|---|
| 列表数据 | 使用 List + ForEach,并提供稳定 key |
| 设置页 | 使用 Form + Section |
| 多字段状态变化 | 使用 state.batch_update(...) |
| 派生搜索结果 | 使用普通函数或 computed |
| 计时器和进度 | 模块级 Timer,回调中更新状态 |
| 高频数值 | 先用 State 做正确,再按需切到 ReactiveState |
| 大文件读取 | 放到按钮动作、任务或后台流程中 |
| 图片和网络 | 先异步加载,再把结果写入状态 |
#避免在 body 中做重活
body() 应该只描述界面。不要在里面同步读取大文件、下载网络内容、递归扫描目录、创建 Timer 或做复杂排序。
已复制
import appui
state = appui.State(items=["main.py", "notes.md", "data.json"], selected="None")
def item_key(name):
return name
def select_item(name):
state.selected = name
def row_view(name):
def select_current():
select_item(name)
return appui.Button(
action=select_current,
content=appui.Text(name).frame(max_width=appui.infinity, alignment="leading"),
).button_style("plain")
def body():
return appui.NavigationStack(
appui.List([
appui.Section("Files", [
appui.ForEach(state.items, row_builder=row_view, key=item_key)
]),
appui.Section("State", [
appui.LabeledContent("Selected", value=state.selected)
]),
]).navigation_title("Files")
)
appui.run(body, state=state, presentation="sheet")
如果数据来自耗时操作,先在事件中准备好,再更新状态。
#控制状态刷新范围
把变化频繁的值放到明确字段里。一次动作改多个字段时用 batch_update,避免中间状态反复重建。
已复制
import appui
state = appui.State(count=0, message="Ready")
def plus():
state.batch_update(count=state.count + 1, message="Updated")
def reset():
state.batch_update(count=0, message="Reset")
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("Counter", [
appui.LabeledContent("Count", value=str(state.count)),
appui.HStack([
appui.Button("增加", action=plus).button_style("bordered_prominent"),
appui.Button("重置", action=reset).button_style("bordered"),
], spacing=12),
], footer=state.message)
]).navigation_title("Refresh Scope")
)
appui.run(body, state=state, presentation="sheet")
#列表和大数据
大列表优先使用原生列表组件。过滤、排序、分组这类派生数据不要在每个 row 里重复计算。
已复制
import appui
rows = [{"id": index, "title": f"Item {index}"} for index in range(1, 101)]
state = appui.State(query="", rows=rows, selected="None")
def set_query(value):
state.query = value
@appui.computed(state, depends_on=["query", "rows"])
def filtered_rows():
keyword = state.query.strip().lower()
if not keyword:
return state.rows
return [row for row in state.rows if keyword in row["title"].lower()]
def row_key(row):
return row["id"]
def select_row(row):
state.selected = row["title"]
def row_view(row):
def select_current():
select_row(row)
return appui.Button(
action=select_current,
content=appui.HStack([
appui.Text(row["title"]).frame(max_width=appui.infinity, alignment="leading"),
appui.Image(system_name="chevron.right").foreground_color("tertiaryLabel"),
]),
).button_style("plain")
def body():
visible = filtered_rows()
return appui.NavigationStack(
appui.List([
appui.Section(f"{len(visible)} results", [
appui.ForEach(visible, row_builder=row_view, key=row_key)
]),
appui.Section("Selection", [
appui.LabeledContent("Selected", value=state.selected)
]),
])
.searchable(text=state.query, on_change=set_query)
.navigation_title("Large List")
)
appui.run(body, state=state, presentation="sheet")
不要把大量行直接塞进普通 VStack 再放进 ScrollView。列表需要稳定的行身份和系统级复用能力。
#Timer 和中频刷新
Timer 放模块级,启动和停止由回调控制,不要在 body() 里创建。
已复制
import appui
state = appui.State(seconds=0, running=False)
def tick():
if state.running:
state.seconds += 1
timer = appui.Timer(interval=1.0, 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=str(state.seconds)),
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)
#高频属性更新
当页面结构稳定、只是已有控件的值持续变化时,可以使用 实时快路径模式。它适合进度、滑块、仪表盘、计时器和状态文本。
关键原则:
- 先保证普通
State版本正确。 - 高频字段再改成
ReactiveState或组件绑定。 - 不要手动调用未公开接口。
- 结构变化仍然交给普通刷新。
已复制
import appui
state = appui.ReactiveState(
title="Meter",
level=(0.0, 1),
peak=(0.0, 2),
)
def update_meter(level, peak):
state.level = level
state.peak = peak
#重型原生视图
VideoPlayer、MapView、WebView、相机预览这类视图不要放进普通列表行里滚动。把它们放在稳定区域,让下方的控制项使用 Form 或 List。
| 场景 | 推荐 |
|---|---|
| 视频播放 | 顶部固定 VideoPlayer,下方 Form 控制区 |
| 地图选点 | 顶部 MapView,下方坐标和操作 |
| WebView 帮助页 | WebView 独立稳定区域 |
| 图表仪表盘 | Chart 或 Canvas 放在固定高度卡片中 |