views — 视图模型与视图 API
创建并管理插件的原生视图模型(ViewModel)与类型化视图。
self.views 提供视图(View)的创建与管理能力。一个视图对应一个在 IDE 中渲染的实例,插件通过 ViewModel 同步其数据模型。
view = self.views.create("my_view")
view.open()常量与协议限制
每次快照、每个补丁事务以及整个视图负载都有硬性上限,超过上限会在发送到宿主之前抛出异常。
| 常量 | 值 | 说明 |
|---|---|---|
MAX_SNAPSHOT_NODES | 20_000 | 单次快照最多节点数 |
MAX_PATCH_OPS | 2_000 | 单个补丁最多操作数 |
MAX_VIEW_PAYLOAD_BYTES | 2MB | 单个快照/补丁负载的最大序列化大小 |
所有越界操作会在本地抛出 ViewProtocolError,不会发送到宿主。常见触发场景:视图已关闭、快照超过节点数上限、单次补丁负载超过字节上限。
创建视图模型
create(view_id, instance_id=None) -> ViewModel
创建一个本地视图模型。view_id 必须是插件贡献的视图 ID。
instance_id 标识视图的具体挂载实例,宿主按 (view_id, instance_id) 定位要渲染的模型,两边必须一致:
- 不传(侧边栏场景):SDK 从 manifest 读取该视图声明的
container,默认成container:<containerId>,与宿主侧边栏的挂载点对齐。绝大多数 UI 插件直接调用self.views.create(view_id).open()即可。 - 显式传入(标签页等场景):使用宿主分配的实例 ID,例如
self.tabs.create_view()回调里拿到的instance.instance_id(形如tab:<n>)。显式值始终优先。 - 兜底:当视图不在 manifest 中,或以 standalone 方式运行(无宿主上下文)时,回退为随机生成。
若手动传入的 instance_id 与宿主实际挂载的实例不一致,快照会被送到一个无人渲染的实例,视图会一直停在加载指示器上。侧边栏视图通常直接省略该参数,交给 SDK 默认对齐。
# 侧边栏:省略 instance_id,自动对齐到 container:<containerId>
view = self.views.create("dashboard")
view.open()revision 与同步模型
ViewModel 追踪 revision。首次发送为完整快照,之后为增量补丁;同一时刻最多有一个补丁在途,等待确认期间产生的操作会合并进下一个补丁,因此快速生产者不会超出宿主处理速度。
视图模型的两类形态
ViewModel 承载的数据节点决定视图在 IDE 中的渲染路径,详见 UI 插件开发:
- 纯数据节点:快照中的节点是普通字典(行、树节点、日志条目)。宿主根据 manifest 的
renderer(native.tree等)选择对应渲染器绘制。 - 组件树:快照中的第一个节点是一个
Component(或其to_json()结果,或带component映射的字典)。宿主直接渲染这棵组件树,忽略renderer。
# 组件树形态
view.snapshot([
Column(
Text("Hello"),
Button(id="go", label="提交", on_press=lambda e: print("pressed")),
),
])两类形态互斥,以第一个节点为准。混用会产生未定义行为。
同步协议
ViewModel 与宿主之间使用快照 + 增量补丁 + 修订号协议(sdk.view.snapshot / sdk.view.patch / ide.view.ack / ide.view.nack / ide.view.resync):
插件 IDE
│ snapshot(revision=1) │
│ ───────────────────────────────>│ 首次:完整快照
│ patch(baseRevision=1→2) │
│ ───────────────────────────────>│ 增量补丁
│<─────────────────────────────── ide.view.ack(revision=2)
│ patch(baseRevision=2→3) │
│ ───────────────────────────────>│
│<─────────────────────────────── ide.view.ack(revision=3)
│ ...若宿主发现修订号断层/异常...│
│<─────────────────────────────── ide.view.nack / ide.view.resync
│ snapshot(重发完整快照) │
│ ───────────────────────────────>│insert/update/remove/move默认立即入队,batch()内操作合并为一个补丁事务发送。- 每次补丁携带
baseRevision与目标revision;宿主确认后revision前移。 - 同一时刻最多有一个补丁在途。等待确认时产生的操作继续排队,并在确认后合并到下一批补丁。
revisionGap表示baseRevision与宿主当前修订不一致,noSnapshot表示该实例没有快照。这两种拒绝会产生ide.view.nack,随后宿主发送ide.view.resync,SDK 重发完整快照。invalidOperation和closed只产生ide.view.nack。SDK 收到补丁错误响应或 nack 后也会用当前本地模型执行resync(),使两端重新收敛。- 性能保护返回
delivery_paused时,SDK 保留已经应用到本地模型的状态,清除在途标记并等待宿主恢复。宿主恢复投递时发送ide.view.resync,SDK 再提交完整快照。 - 视图隐藏时,变更只应用到本地模型并设置
_needs_snapshot。恢复可见后发送一份新快照,不逐条补发隐藏期间的操作。
snapshot() 在视图已关闭、节点数超过 MAX_SNAPSHOT_NODES 或负载超过 MAX_VIEW_PAYLOAD_BYTES 时,会在发送前本地抛出 ViewProtocolError。补丁负载同样受 MAX_VIEW_PAYLOAD_BYTES 限制,超限时也会抛 ViewProtocolError。
ViewModel 属性
| 属性 | 说明 |
|---|---|
view_id | 视图 ID |
instance_id | 实例 ID |
revision | 当前已确认的修订号 |
nodes | 当前模型节点列表(副本) |
visible | 视图当前是否可见 |
has_in_flight | 是否有补丁在途 |
pending_count | 排队等待发送的操作数 |
route | 当前路由 |
route_stack | 路由栈(副本) |
route_params | 当前路由参数(副本) |
生命周期
| 方法 | 说明 |
|---|---|
open(callback=None) | 打开视图 |
close(callback=None) | 关闭视图 |
snapshot(nodes, revision=None, callback=None) | 发送完整快照并重置补丁状态 |
view.open()
view.snapshot([
{"id": "row_1", "label": "Hello"},
{"id": "row_2", "label": "World"},
])数据变更
| 方法 | 说明 |
|---|---|
insert(id, data, index=None) | 插入节点 |
update(id, data) | 更新节点 |
remove(id) | 删除节点 |
move(id, index) | 移动节点到指定索引 |
batch() | 上下文管理器,将内部操作合并为单个补丁事务 |
flush() | 立即发送排队操作(除非已有补丁在途),返回是否已发送 |
with view.batch():
view.insert("a", {"label": "A"})
view.insert("b", {"label": "B"})
view.move("a", 1)batch() 中的操作在退出时作为一个补丁事务发送。当补丁在途时,操作会保持排队,并在确认后合并进下一个补丁。
事件与组件调用
| 方法 | 说明 |
|---|---|
on_event(component_id, event, handler) | 注册组件事件处理函数,返回 handler |
off_event(component_id, event) | 注销组件事件处理函数 |
dispatch_event(component_id, event, payload) | 分发事件,返回是否有处理函数执行 |
invoke_component(component_id, method, arguments=None, callback=None) | 调用已挂载组件的类型化操作 |
可见性与路由
| 方法 | 说明 |
|---|---|
on_visibility(handler) | 注册可见性变化回调(handler(visible)) |
on_route(handler) | 注册路由同步回调,handler 接收 (route, params) |
push_route(route, params=None, callback=None) | 入栈路由 |
replace_route(route, params=None, callback=None) | 替换当前路由 |
goto_route(route, params=None, callback=None) | 跳转到指定路由 |
pop_route(callback=None) | 弹出当前路由 |
路由以实例为单位:宿主持有权威路由栈,并通过路由同步帧推回,本地属性只是最近一次同步的镜像。
类型化视图 Facade
self.views 还提供类型化视图的快速入口,返回对应的 RendererView 子类(见 native_views):
| 方法 | 说明 |
|---|---|
outline(view_id, title="Outline", actions=()) | 大纲视图 |
tree(view_id, title="Tree", actions=(), expanded_ids=None, selected_id=None, indent=None, searchable=None, empty_label=None) | 树形视图 |
virtual_list(view_id, title="List", actions=(), item_count=None, item_height=None, selected_id=None, empty_label=None) | 虚拟列表 |
table(view_id, title="Table", columns=(), actions=(), row_count=None, row_height=None, show_header=None, selected_id=None, sort_column=None, sort_ascending=None, empty_label=None) | 表格视图 |
form(view_id, title="Form", actions=()) | 表单视图 |
markdown(view_id, title="Markdown", actions=()) | Markdown 视图 |
log(view_id, title="Log", actions=(), item_height=None, empty_label=None) | 日志视图 |
variable_inspector(view_id, title="Device Variables", actions=()) | 变量检查器 |
tree = self.views.tree("file_tree", searchable=True)
log = self.views.log("console", empty_label="暂无输出")RendererView.set_items() 使用节点 id 计算带顺序的增量差异。同一个业务对象在连续刷新中必须保持相同 ID;重复 ID 或随刷新变化的 ID 会破坏选择、展开状态和补丁定位。事件回调接收对应的类型化节点,因此可直接访问 node.id、node.label 等属性。
Facade 在普通业务节点之外还会生成带语义角色的协议节点。appBarAction 和 appBarMenu 描述标题栏操作,placeholder 描述空状态,viewConfig 携带渲染器配置,contextMenuProvider 表示视图支持动态上下文菜单。IDE 渲染器必须先识别这些角色,再处理普通数据节点;内部开发时不能把它们当作用户数据过滤掉。
查找视图模型
get(instance_id, view_id=None) -> ViewModel?
按实例 ID 查找已创建的视图模型;传入 view_id 时按 (view_id, instance_id) 精确匹配,否则要求实例 ID 唯一。
model = self.views.get(instance_id, view_id="dashboard")侧边栏挂载点使用 container:<containerId>。标签页由宿主创建,回调返回形如 tab:<n> 的实例 ID;插件必须使用该 ID 创建或查找模型。view_id 相同但 instance_id 不同的视图拥有独立的修订号、路由栈、可见性和组件状态。
使用示例
class MyPlugin(UiPlugin):
def on_start(self):
self.view = self.views.create("my_view")
self.view.open()
self.view.snapshot([
{"id": "greeting", "label": "PyriteSDK"},
])
self.view.on_visibility(self._on_visibility)
def _on_visibility(self, visible):
print(f"视图可见性: {visible}")实现位置
- SDK
src/pyrite_sdk/api/view.py维护本地模型、修订号、在途补丁、路由和恢复状态。 - SDK
src/pyrite_sdk/api/native_views.py实现类型化节点、稳定 ID 差异和渲染器协议角色。 - IDE
lib/core/sdk/view_model_store.dart以插件、会话和视图实例为键保存权威模型,并以事务方式应用补丁。 - IDE
lib/core/sdk/api/view_api.dart校验消息、发送 ack、nack 与 resync。 - IDE
lib/features/plugin_view/包含视图表面、渲染器注册表和通用组件构建器。