- 状态:已定(2026-09-16;2026-09-24 按 ADR-006 §2.11 改 §6:运行时随宿主,K0 版本核对删去);bench 的 verify 模式与 Kotlin 解析器都以此为准
- 来源:ADR-001(五种 op 草案)、ADR-002 §3.6(JSON 文本、数组形态、prop 摊平)、ADR-003 §3.4 / §3.5(写入时转换、E5、事件标记)、ADR-004 §3.2(
x命令 op) - 两侧:JS 运行时的
flush()产出;KotlinNodeTree.apply流式解析并写入
一次 flush = 一条消息 = 一个 JSON 数组,元素是 op;经 J1 __host_apply(json) 交出。没有 op 时不发消息。
[["c",1,"Column"],["c",2,"Text"],["p",2,"text","Count: 0"],["c",3,"Button"],["p",3,"text","+1"],["p",3,"onClick",true],["i",1,2,0],["i",1,3,1],["i",0,1,0]]| op | 形态 | 语义 |
|---|---|---|
| create | ["c", id, type] |
新建节点,未挂载。type 为注册表里的类型名 |
| prop | ["p", id, key, value] |
设置 prop。value 为 string / number / boolean / null;null 表示恢复 schema 默认值。key 匹配 /^on[A-Z]/ 时是事件标记,value 只能是 boolean,true 表示 JS 侧有 handler |
| insert | ["i", parent, id, index] |
把 id 插到 parent 的 children 的 index 位置(插入后的位置,即 children.add(index, node))。parent 为 0 表示页面根容器 |
| move | ["m", parent, id, index] |
id 已是 parent 的 child;先从 children 移除,再插到 index(移除后列表里的位置) |
| remove | ["r", id] |
从父节点 children 移除并连同子树删除。JS 只对子树根发一条,不逐个发后代 |
| command | ["x", id, name, args] |
对节点发一次性命令,args 是内嵌的 JSON 对象,字段为 string / number / boolean / null(ADR-004 §3.2) |
- 正整数,JS 侧
nextId++分配,页内唯一,不回收不复用;0 保留为根容器 - 根容器只能有一个 child(页面根节点),
mount时插入一次 r之后该 id 及其子树的 id 永远不再出现;再出现是 E5
| 类型 | JSON | 约定 |
|---|---|---|
| 文本 | string | UTF-8 |
| 数字 | number | 尺寸一律 dp,字号由 schema 的 sp() 决定(ADR-003 §3.4),不靠后缀 |
| 布尔 | boolean | |
| 颜色 | string | #RRGGBB 或 #AARRGGBB |
| 枚举 | string | schema 声明的枚举名 |
| 空 | null | 恢复默认 |
prop 里不出现对象与数组(ADR-002 "prop 摊平");x 的 args 是唯一的对象形态,且只有一层。
JS 侧承诺:
- 一个节点的
c先于它的任何p/i/m/r/x - 节点的全部初始
p紧跟在它的c之后、先于把它插入父节点的i(Kotlin 拿到i时节点已完整) - 子树自底向上:children 先
c+p+ 插入到父,父再插入到祖父。挂载时整棵树最后一条是["i", 0, root, 0] m/r的对象一定是当前存在且已挂载的节点- 同一事务里一个 prop 可能出现多次(同一 flush 内多次赋值不合并),按序应用即得终值
- 事务原子:flush 中途抛错(E2)则整条消息不发
Kotlin 侧承诺(ADR-003 §3.2):
- 整条消息在一个
withMutableSnapshot内按序应用,其他线程要么看到应用前、要么看到应用后 - 节点表只由 JS 线程写;
x进节点的命令队列而非 props,由组件在组合后消费 requiredprop 只在c所在消息应用完毕时校验一次
本协议没有独立的版本号,也不需要。运行时模块(tinyui-core / tinyui-native)随宿主:它们的字节码与 Kotlin 侧出自同一次库构建(ADR-006 §2.11),两侧之间的全部约定(本文的 op、K 入口、__host_* 函数、mount 清单格式)天然同版本,改动两侧同一 PR,不做运行时核对。页面只经运行时公开面与运行时打交道,兼容规则见 runtime-api.md §11。
schema 层面的变化(新组件、新 prop、新事件)随 tinyui 版本走,页面构建时的 tinyui 与宿主兼容即可用(同 major 且宿主不低于包,updates.md §1.1);同一版本内宿主组件的差异由 ADR-003 的清单下发与 E5 跳过处理。
| 情况 | 处理 |
|---|---|
c 的 type 未注册 |
建 Placeholder 节点占位,上报 |
p / i / m / r / x 的 id 不存在 |
跳过,上报(附 op 原文) |
p 的 key 不在该类型的 schema 里 |
跳过,上报 |
p 的 value 转换失败(类型、枚举名、颜色格式) |
跳过,上报(附 type / key / 原值) |
p 事件标记的 value 不是 boolean,或事件不在 schema |
跳过,上报 |
i / m 的 index 越界 |
夹到 [0, size],上报 |
i 的 id 已有父节点 |
跳过,上报 |
x 的 name 不在 schema,或 args 字段不符 |
跳过,上报 |
| 消息不是合法 JSON 数组 | 整条丢弃,上报;页面继续 |
宿主永不因坏消息崩溃;所有上报进 ADR-002 的同一个 sink,附页面实例 id。
__host.apply(json, count)的count参数去掉- 新增
x p的null语义(恢复默认)是新定的:运行时对undefined与null都发null,Kotlin 写回 schema 默认值,组件永远读不到 null。推论:schema 不能声明"合法值就是 null"的 prop,"空"用空串或枚举值表达- 事件标记
false(撤销 handler):Kotlin 解析器实现它(从事件集合删除、卸下手势),但 v1 运行时没有撤销 API、不会主动发。协议先于运行时能力,将来on*支持动态绑定时不用改版本