PythonIDE Docs
中文
简体中文

状态管理

State、ReactiveState、Timer 和批量更新的使用边界。

appui 的界面由状态驱动:状态变了,body() 重新执行,View 树重新生成。状态代码要短、集中、可预测。

#预期效果

示例会展示表单字段、列表选择、搜索结果、滑块和计时器如何由状态驱动刷新。

#选择哪种状态

用法选择说明
表单字段、开关、当前 tab、普通计数State最常用,写入后触发重建。
高频数值或大数组ReactiveState可走实时快路径,减少整树重建压力。
不想触发重建的对象Ref保存句柄、缓存对象、一次性资源。
列表 / 字典增删改ObservableList / ObservableDictState 会自动包装 list/dict,让局部变更也能通知界面。
派生值computed从状态计算,不手动同步副本。
状态变化后的副作用effect用于日志、保存、触发外部动作。

普通页面先用 State。只有高频更新或大数据刷新已经明显卡顿时,再考虑 ReactiveState

#State

State 适合绝大多数页面状态。控件读取当前字段,on_change 或按钮回调写回字段。

python
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

python
state.batch_update(name="Demo", enabled=True, count=0)
snapshot = state.to_dict()

#ObservableList / ObservableDict

列表和字典放进 State 后会被包装成可观察容器。appendpopupdate 等局部变更也能通知界面。

python
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 保存“不属于界面事实源”的对象,写入不会触发重建。

python
timer_ref = appui.Ref(None)
cache_ref = appui.Ref({})

典型用途是保存 timer、网络任务、播放器句柄、滚动代理等。不要把需要显示到界面的值放进 Ref

#computed 与 effect

computed 用来声明派生值,避免维护两份状态。

python
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")

python
appui.Slider(**appui.bind(state, "progress"), minimum=0, maximum=100)

TextFieldTogglePicker 的参数名不是 value,通常直接传当前值和 on_change

ReactiveState 适合频繁更新的属性,例如 slider 值、拖动坐标、图表数据。它可以绑定实时属性通道,减少高频整树重建。

python
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。

python
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,不使用过滤后的下标。

#下一步