
前面的几篇文章主要在看 UI 链路:Pager 怎么创建,状态怎么触发更新,布局怎么计算,Bridge 怎么把 View 操作下发到 Render。真实业务里还有另一类调用也很常见:网络请求、路由跳转、本地存储、通知、文件。
这些能力没法都放在 Kotlin commonMain 里实现。Android、iOS、小程序、Web 都有自己的网络和存储接口。KuiklyUI 如果只解决 UI 渲染,不解决这些系统能力,业务还是会被平台差异拆开。
Module 体系就是为这条链路准备的。Kotlin 侧保留统一 API,平台侧负责执行。它和 View/Render 链路一样,也是一条跨端边界;只是这一次传递的不再是 View 操作,而是一次系统能力调用。
业务侧调用网络请求时,看起来就是一次普通的 Kotlin 方法调用。真正执行时,Kotlin 侧的网络模块并不直接发请求,它只负责把这次调用整理成统一协议,再交给 Render 侧的平台模块处理。
如果放到前端跨端体系里看,Module 对应的是 NativeModules 这一层。业务不直接接触平台 API,而是通过统一入口表达意图:我要发请求、我要跳页面、我要读本地存储。KuiklyUI 的特殊点在于,这个统一入口放在 Kotlin commonMain,调用链继续沿用它自己的 Bridge 协议。

图里先看两个方向:Kotlin 调 Native 时走模块调用通道,Native 回 Kotlin 时走 callback 通道。中间没有把 Kotlin lambda 真的传到 Native,只传了一个回调标识。
Kotlin 侧的 Module 作用域落在 Pager 上。可以把 Pager 理解成页面级的模块容器:它既保存模块工厂,也缓存已经创建过的模块实例。
页面创建时,Pager 会先注册内置模块和外部模块。模块本身不会在页面启动时全部创建,只有业务真正用到某个模块时,Pager 才会按名称找到工厂、创建实例、注入页面上下文,再缓存起来。后续同一个页面再次拿同名模块,返回的就是同一个实例。
这个设计带来几个直接效果。
模块是按需创建的。一个页面如果只用路由和本地存储,就不会提前创建其他模块。
模块也是按页面隔离的。两个 Pager 各自持有自己的模块实例,callback 也按页面分桶。页面销毁时,Pager 会清理这个页面下的 callback 和模块缓存。
还有一个容易忽略的点:性能追踪上下文也在这里注入。Module 调用开始、结束、callback 开始、结束,都可以落到页面级性能追踪里。
Module 基类本身很薄,它不关心网络、路由、存储这些业务含义,只承担跨 Bridge 调用前后的整理工作。
第一步是处理参数。普通业务参数会被整理成一段 JSON,再作为协议内容传给平台侧。这里不需要把每个字段都展开,关键是看清楚:Module 做的是参数收拢,不是能力执行。
第二步是保存 callback。业务传进来的 Kotlin lambda 始终留在 Kotlin 侧,Module 只把它登记起来,并生成一个可以跨端传递的回调标识。
第三步是调用 Bridge。一次普通 Module 调用,最终会被整理成页面、模块、方法、参数、回调、是否同步这几类信息,再交给 Bridge 下发。
Bridge 不理解网络、路由这些业务含义。它只负责把协议送到 Render。Render 再找到平台侧模块,把调用交给真正的 Android、iOS、Web 或鸿蒙实现。
以网络请求为例,调用链可以压缩成五步:
业务代码
↓
Kotlin Module 整理参数并登记 callback
↓
Bridge 下发模块调用协议
↓
Render 找到平台网络模块并执行请求
↓
请求结果通过 callback 回到 Kotlin这里容易误解的是 callback 的处理方式。Kotlin lambda 没有跨语言传递,它始终留在 Kotlin 侧。Native 只拿到一个回调标识,请求结束后再把这个标识和结果一起带回来。Kotlin 侧再根据页面和回调标识找回原来的 lambda。
默认 callback 是一次性的。普通请求回调执行完就会清理;通知订阅这类长期场景会把 callback 保留下来,直到业务主动取消订阅,或者页面销毁时统一清理。
这就是 Module 和 Bridge 之间的一次典型配合:Bridge 只传协议,Module 负责把业务 API 包装成协议,Render 侧负责把协议转成平台能力。
Kotlin 侧发出模块调用后,各平台 Render 会做一层很薄的包装。
Android 和 Web 的流程都比较接近:Render 先把回调包装成平台侧可以持有的对象,再根据模块名找到对应实现。平台模块执行完之后,再通过统一 callback 通道把结果送回 Kotlin。
iOS 侧通过协议暴露模块能力,平台模块只需要接收方法、参数和 callback,不需要理解 Kotlin 侧具体的模块类。
鸿蒙侧多了一层 C++/ArkTS 转发,但核心逻辑没有变:先在模块注册表里找到实现,如果能力在 ArkTS 层,再继续向 ArkTS 转发。
这些实现细节不完全一样,但抽象是一样的:Render 侧只认字符串协议,平台模块执行具体能力,再通过 callback 把结果送回 Kotlin。
前面看到的是标准链路:Kotlin 侧拼参数,Bridge 下发,Render 侧执行,再通过 callback 回来。回到 Module 这一层,这条链路有几种入口,差别主要在参数格式和是否同步。
最常见的是异步 JSON 通道。网络请求、路由跳转、通知订阅这类能力通常都走这条路径,参数容易调试,也方便跨平台处理,结果通过 callback 返回。
轻量查询可以走同步调用。本地 KV、内存缓存读取、编解码这类低耗时操作,不一定要让业务代码绕一层异步 callback。但同步调用会阻塞当前链路,所以边界要放得很窄。
二进制数据没有继续塞进 JSON,而是走数组通道。这样可以直接携带基础类型和字节数组,避免先 base64 再跨 Bridge。
还有一类兼容入口,主要服务和 TDF 体系互通的场景。放在这篇文章里只需要知道它仍然复用模块名和方法名的分发思路,后面不再展开。
KuiklyUI 内置了多种模块。这里不逐个罗列,只挑和上面几种调用形态直接相关的场景看。
网络请求对应异步调用。普通请求走 JSON callback,二进制请求走数组通道。它刚好覆盖了 Module 里最常见的两条数据路径。
本地存储对应轻量同步调用。读一个本地字符串如果也写成异步 callback,业务代码会被迫绕一层;但反过来,如果把耗时能力都写成同步调用,Bridge 链路就会被拖住。所以本地存储更像是在说明同步调用的边界。
通知订阅对应长期 callback。普通 callback 执行完就清理,订阅不能这样做,所以这类模块必须提供明确的取消入口,同时清理 Kotlin 侧 callback 和 Native 侧监听。
路由对应没有复杂回包的原生动作。Kotlin 侧只表达跳转或关闭页面这类意图,平台侧决定具体怎么落到 Activity、ViewController 或 Web 路由上。
这几个例子基本覆盖了文章主线:异步 JSON、同步返回、二进制参数、长期 callback、平台动作。其他内置模块大多只是换了一组模块名和方法名,不影响这条调用链,不在这里单独展开。
讲到通知订阅,就自然会碰到 callback 生命周期。Module 更容易出问题的地方在异步结果返回时机。网络请求还没完成,页面可能已经销毁;通知监听可能长期存在;其他模块的 callback 也可能晚于页面生命周期返回。
KuiklyUI 的保护主要依赖页面 ID。所有 callback 都按页面分组保存。页面销毁时,这个页面下登记过的 callback 会被一次性清掉。后续 Native 再回调,如果 Kotlin 侧已经找不到对应记录,就不会继续执行业务 lambda。
这能挡住大部分页面销毁后的异步回调问题,但长期订阅场景仍然要主动清理。取消订阅时,不只要删除 Kotlin 侧 callback,也要通知 Native 侧移除监听。否则 Kotlin 侧虽然兜底清了,平台资源仍可能继续占用。
自定义 Module 也要遵守这个边界。不要在构造期依赖页面上下文,因为注入发生在模块创建之后;不要长期持有 View 强引用,优先保存 tag 或业务 ID;如果一个 callback 会被多次触发,就要明确设计取消方法。
到这里,Module 的主线已经闭合:Pager 管实例,Module 包协议,Bridge 传参数,Render 执行能力,callback 再回到 Kotlin。最后看几个取舍。
先看协议复用。新增模块通常不需要改 Bridge,只要约定模块名和方法名。代价是 Native 侧要做二次分发,方法名也缺少编译期校验。
第二个取舍是页面作用域。模块跟着页面走,callback 路由、性能埋点和生命周期清理都更清楚。代价是每个页面会持有自己的模块实例,跨页面共享状态不能直接塞在 Module 实例里,需要放到平台缓存或业务公共层。
第三个取舍是参数形态。JSON 调试方便,也容易跨平台;二进制数据如果也走 JSON,成本就会很高,所以 KuiklyUI 给二进制单独留了数组通道。
第四个取舍是底层坚持 callback。业务层可以再包一层更顺手的异步 API,但协议层不需要理解这些上层写法。平台侧仍然只需要实现方法、参数、callback 这几个概念。
这些选择并不追求强类型 RPC 的完整性。KuiklyUI 更像是把跨端业务里最常见的系统能力调用整理成一条稳定路径:够轻、够通用,也能留下二进制和兼容通道的出口。
Module 是 KuiklyUI 调用原生能力的统一入口。业务在 Kotlin commonMain 里拿到模块实例,Module 负责整理参数、保存 callback、调用 Bridge;Render 侧根据协议找到平台实现;结果再通过 callback 回到 Kotlin。
它和 View/Render 链路的分工一致:Core 表达意图,Bridge 传递协议,平台侧执行具体能力。区别在于,View 链路处理的是 UI 节点和属性,Module 链路处理的是系统能力和业务回调。
补上 Module 之后,KuiklyUI 的主线就不只覆盖 UI 更新,也覆盖了业务调用平台能力的路径。下一篇继续看动画系统。动画同样跨过 Core、Bridge 和 Render,但它的执行方式和普通属性更新不太一样。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。