从 ui 迁移到 appui
命令式 旧版手动布局 API 写法迁移到声明式 AppUI。
ui 是命令式控件树,appui 是声明式 View 树。迁移时不要逐行翻译控件操作;先把界面状态整理出来,再用 body() 描述当前状态下的界面。
#对照表
| ui 写法 | appui 写法 |
|---|---|
| 创建控件后设置属性 | 在构造函数和修饰符链里声明属性 |
view.add_subview(...) | VStack/HStack/ZStack/List/Form 组合子 View |
| 回调里直接改控件 | 回调里改 State |
| 手动 push/present | NavigationStack、sheet、alert |
| 保存控件实例用于后续修改 | 保存状态或 Ref |
| frame 手算布局 | Stack/Grid/frame/padding |
#最小迁移例子
旧 ui 思路:
已复制
import ui
count = 0
label = ui.Label(text="0")
def add(sender):
global count
count += 1
label.text = str(count)
button = ui.Button(title="Add")
button.action = add
appui 写法:
已复制
import appui
state = appui.State(count=0)
def increment():
state.count += 1
def body():
return appui.VStack([
appui.Text(str(state.count)).font("largeTitle").bold(),
appui.Button("Add", action=increment),
], spacing=16).padding()
appui.run(body, state=state)
重点不是 Label -> Text,而是“控件文本”变成了 state.count 的投影。
#列表迁移
旧写法通常是维护 table 数据源并手动刷新。appui 里直接让列表来自状态。
已复制
import appui
state = appui.State(items=["Milk", "Coffee"])
def add_item():
state.items = list(state.items) + [f"Item {len(state.items) + 1}"]
def item_key(item):
return item
def row_view(item):
return appui.Text(item)
def body():
return appui.NavigationStack(
appui.List([
appui.Section("Items", [
appui.ForEach(state.items, row_builder=row_view, key=item_key)
])
])
.navigation_title("List")
.toolbar([
appui.ToolbarItem(
placement="navigation_bar_trailing",
content=appui.Button("Add", action=add_item),
)
])
)
appui.run(body, state=state)
需要增删改的列表要么整体替换 state.items,要么使用 ObservableList;不要原地修改普通 list 后期待 UI 自动刷新。
#表单迁移
ui.TextField 的 action / delegate 通常迁移成绑定。
已复制
import appui
state = appui.State(name="", notifications=True)
def set_name(value):
state.name = value
def set_notifications(value):
state.notifications = value
def body():
return appui.Form([
appui.Section("Profile", [
appui.TextField("Name", text=state.name, on_change=set_name),
appui.Toggle("Notifications", is_on=state.notifications, on_change=set_notifications),
]),
appui.Section("Preview", [
appui.Text(f"Hello, {state.name or 'Guest'}"),
]),
]).navigation_title("Profile")
appui.run(body, state=state)
表单字段用当前值加 on_change 写回;不要在多个地方维护同一个字段的副本。
#导航迁移
命令式 present/push 迁移到数据化导航:
已复制
import appui
path = appui.NavigationPath()
state = appui.State(selected="A")
def open_detail():
state.selected = "A"
path.append("detail")
def go_back():
path.pop()
def detail_destination(value):
return appui.VStack([
appui.Text(f"Detail {state.selected}").font("title2"),
appui.Button("Back", action=go_back),
], spacing=12).padding()
def body():
return appui.NavigationStack(
content=appui.VStack([
appui.Text("Home"),
appui.Button("Open detail", action=open_detail),
], spacing=16).padding().navigation_title("Home"),
path=path,
destinations={
"detail": detail_destination,
},
)
appui.run(body, state=state)
如果只是固定页面跳转,也可以用 NavigationLink。
#迁移步骤
- 列出页面状态:文本、开关、选中项、列表数据、弹层显示状态。
- 把旧控件树改成
body()返回的 View 树。 - 把“修改控件属性”的回调改成“修改 State”。
- 把 frame 手算布局改成 Stack/Grid/ScrollView。
- 把 push/present 改成 NavigationStack/sheet/alert。
- 每次迁移一屏,保留可运行入口。
#不要直接翻译的写法
| 旧习惯 | 问题 | appui 改法 |
|---|---|---|
label.text = ... | 绕过状态源 | Text(f"{state.value}") |
| 全局保存控件对象 | 重建后对象可能失效 | 保存 State / Ref |
| 手动设置每个 frame | 不适配设备 | Stack + frame + padding |
| 回调里同时改很多字段 | 中间状态闪动 | state.batch_update(...) |
| 复制其他框架参数名 | Python API 可能不同 | 查 appui API 参考 |