一个真实的 Android 模块通常不是"Java 完全结束后才开始 Kotlin":
Activity、旧 SDK 封装和回调接口仍是 Java,新的 Repository、ViewModel 与状态流逐步用 Kotlin 编写。本文以这种混合模块为起点,讨论怎样把边界迁清楚,而不是把所有文件机械转换。
一句话结论: 把 Kotlin 放在可控的 facade 与异步适配层,显式处理 Java 平台类型、取消和生命周期,再按调用边界逐步替换,才能让迁移同时保持 Java 可用性与 Android 行为稳定。
本文以 Kotlin 2.x 语言与协程/Flow 的概念为边界;repeatOnLifecycle 等收集辅助函数属于 AndroidX Lifecycle,不是 Kotlin 编译器的能力,具体 API 以项目依赖版本为准。
先画出增量迁移的调用流
不要先问"哪种语法能自动转换",先问 Java 调用从哪里进入、谁承诺兼容、异步结果最后由谁消费。下图中 Kotlin facade 是迁移的收敛点:Java 继续依赖稳定入口,新 Kotlin 逻辑在其后演进。
#mermaid-svg-B1G0t3Re5K2P9PYd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-B1G0t3Re5K2P9PYd .error-icon{fill:#552222;}#mermaid-svg-B1G0t3Re5K2P9PYd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-B1G0t3Re5K2P9PYd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-B1G0t3Re5K2P9PYd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-B1G0t3Re5K2P9PYd .marker.cross{stroke:#333333;}#mermaid-svg-B1G0t3Re5K2P9PYd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-B1G0t3Re5K2P9PYd p{margin:0;}#mermaid-svg-B1G0t3Re5K2P9PYd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-B1G0t3Re5K2P9PYd .cluster-label text{fill:#333;}#mermaid-svg-B1G0t3Re5K2P9PYd .cluster-label span{color:#333;}#mermaid-svg-B1G0t3Re5K2P9PYd .cluster-label span p{background-color:transparent;}#mermaid-svg-B1G0t3Re5K2P9PYd .label text,#mermaid-svg-B1G0t3Re5K2P9PYd span{fill:#333;color:#333;}#mermaid-svg-B1G0t3Re5K2P9PYd .node rect,#mermaid-svg-B1G0t3Re5K2P9PYd .node circle,#mermaid-svg-B1G0t3Re5K2P9PYd .node ellipse,#mermaid-svg-B1G0t3Re5K2P9PYd .node polygon,#mermaid-svg-B1G0t3Re5K2P9PYd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-B1G0t3Re5K2P9PYd .rough-node .label text,#mermaid-svg-B1G0t3Re5K2P9PYd .node .label text,#mermaid-svg-B1G0t3Re5K2P9PYd .image-shape .label,#mermaid-svg-B1G0t3Re5K2P9PYd .icon-shape .label{text-anchor:middle;}#mermaid-svg-B1G0t3Re5K2P9PYd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-B1G0t3Re5K2P9PYd .rough-node .label,#mermaid-svg-B1G0t3Re5K2P9PYd .node .label,#mermaid-svg-B1G0t3Re5K2P9PYd .image-shape .label,#mermaid-svg-B1G0t3Re5K2P9PYd .icon-shape .label{text-align:center;}#mermaid-svg-B1G0t3Re5K2P9PYd .node.clickable{cursor:pointer;}#mermaid-svg-B1G0t3Re5K2P9PYd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-B1G0t3Re5K2P9PYd .arrowheadPath{fill:#333333;}#mermaid-svg-B1G0t3Re5K2P9PYd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-B1G0t3Re5K2P9PYd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-B1G0t3Re5K2P9PYd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-B1G0t3Re5K2P9PYd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-B1G0t3Re5K2P9PYd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-B1G0t3Re5K2P9PYd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-B1G0t3Re5K2P9PYd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-B1G0t3Re5K2P9PYd .cluster text{fill:#333;}#mermaid-svg-B1G0t3Re5K2P9PYd .cluster span{color:#333;}#mermaid-svg-B1G0t3Re5K2P9PYd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-B1G0t3Re5K2P9PYd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-B1G0t3Re5K2P9PYd rect.text{fill:none;stroke-width:0;}#mermaid-svg-B1G0t3Re5K2P9PYd .icon-shape,#mermaid-svg-B1G0t3Re5K2P9PYd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-B1G0t3Re5K2P9PYd .icon-shape p,#mermaid-svg-B1G0t3Re5K2P9PYd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-B1G0t3Re5K2P9PYd .icon-shape .label rect,#mermaid-svg-B1G0t3Re5K2P9PYd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-B1G0t3Re5K2P9PYd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-B1G0t3Re5K2P9PYd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-B1G0t3Re5K2P9PYd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Java 页面或旧业务
Kotlin Facade
Java SDK 或旧 Repository
Coroutine Adapter
Flow 状态流
AndroidX 生命周期收集
界面渲染
谁创建:迁移团队先创建一个面向 Java 的 Kotlin facade。谁持有:Java 调用方只持有它暴露的 Java 友好 API;ViewModel 持有状态流。何时触发:旧页面仍按原事件调用 facade。结果交给谁:由 viewLifecycleOwner 所属的 UI 收集者渲染,而不是让 Java callback 直接跨越多个页面层。
迁移时真正要守住的边界
Kotlin 的类型系统会把"可能为空"写进签名,但 Java 的未标注返回值进入 Kotlin 后常成为平台类型,例如 Java String getName() 在 Kotlin 看起来近似 String!。它既可能被当成 String,也可能在运行时是 null,所以它是需要归一化的外部边界,不是"已经安全"的 Kotlin 值。
#mermaid-svg-cvgr5lJOCNqw9dR1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cvgr5lJOCNqw9dR1 .error-icon{fill:#552222;}#mermaid-svg-cvgr5lJOCNqw9dR1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cvgr5lJOCNqw9dR1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .marker.cross{stroke:#333333;}#mermaid-svg-cvgr5lJOCNqw9dR1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cvgr5lJOCNqw9dR1 p{margin:0;}#mermaid-svg-cvgr5lJOCNqw9dR1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .cluster-label text{fill:#333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .cluster-label span{color:#333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .cluster-label span p{background-color:transparent;}#mermaid-svg-cvgr5lJOCNqw9dR1 .label text,#mermaid-svg-cvgr5lJOCNqw9dR1 span{fill:#333;color:#333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .node rect,#mermaid-svg-cvgr5lJOCNqw9dR1 .node circle,#mermaid-svg-cvgr5lJOCNqw9dR1 .node ellipse,#mermaid-svg-cvgr5lJOCNqw9dR1 .node polygon,#mermaid-svg-cvgr5lJOCNqw9dR1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cvgr5lJOCNqw9dR1 .rough-node .label text,#mermaid-svg-cvgr5lJOCNqw9dR1 .node .label text,#mermaid-svg-cvgr5lJOCNqw9dR1 .image-shape .label,#mermaid-svg-cvgr5lJOCNqw9dR1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cvgr5lJOCNqw9dR1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cvgr5lJOCNqw9dR1 .rough-node .label,#mermaid-svg-cvgr5lJOCNqw9dR1 .node .label,#mermaid-svg-cvgr5lJOCNqw9dR1 .image-shape .label,#mermaid-svg-cvgr5lJOCNqw9dR1 .icon-shape .label{text-align:center;}#mermaid-svg-cvgr5lJOCNqw9dR1 .node.clickable{cursor:pointer;}#mermaid-svg-cvgr5lJOCNqw9dR1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .arrowheadPath{fill:#333333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cvgr5lJOCNqw9dR1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cvgr5lJOCNqw9dR1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cvgr5lJOCNqw9dR1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cvgr5lJOCNqw9dR1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cvgr5lJOCNqw9dR1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cvgr5lJOCNqw9dR1 .cluster text{fill:#333;}#mermaid-svg-cvgr5lJOCNqw9dR1 .cluster span{color:#333;}#mermaid-svg-cvgr5lJOCNqw9dR1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cvgr5lJOCNqw9dR1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cvgr5lJOCNqw9dR1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cvgr5lJOCNqw9dR1 .icon-shape,#mermaid-svg-cvgr5lJOCNqw9dR1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cvgr5lJOCNqw9dR1 .icon-shape p,#mermaid-svg-cvgr5lJOCNqw9dR1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cvgr5lJOCNqw9dR1 .icon-shape .label rect,#mermaid-svg-cvgr5lJOCNqw9dR1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cvgr5lJOCNqw9dR1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cvgr5lJOCNqw9dR1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cvgr5lJOCNqw9dR1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Java 未标注返回值
Kotlin 平台类型
Facade 空值归一化
Kotlin 可空模型
显式默认值或错误
业务与界面
谁创建:Java API 创建原始对象或返回值。谁持有:Kotlin facade 只在最短范围内接触平台类型。何时触发:每次跨 Java/Kotlin 边界。结果交给谁:后续 Kotlin 代码只接收 String?、Result<T> 或明确的领域状态。@Nullable/@NonNull 等注解能改善编译器推断,但无法替代运行时契约与测试。
语言、编译器与 AndroidX 各管什么
| 层次 | 源码锚点或代表 API | 回答的问题 | 迁移建议 |
| --- | --- | --- |
| Java 调 Kotlin | ProfileFacade、@JvmStatic、@JvmOverloads | Java 怎样找到并调用 Kotlin | 把 Java 入口做成显式方法或重载,不让调用者猜 Companion |
| Kotlin 调 Java | Java getter、Android framework API、平台类型 | 空值和受检异常怎样进入 Kotlin | 入口即做 nullable/exception 归一化,别把平台类型传远 |
| callback 转挂起 | suspendCancellableCoroutine、CancellableContinuation、one-shot gate | 谁登记回调,谁解除登记 | 适配器用单次终态门禁,取消时注销 |
| 协程取消 | CoroutineScope、Job、viewModelScope | 谁拥有并终止请求 | 让 ViewModel 或明确 use-case scope 拥有 Job,禁止 GlobalScope |
| Flow 收集 | Flow.collect、StateFlow | 最新状态交给谁 | ViewModel 暴露流,UI 层消费流 |
| 生命周期边界 | repeatOnLifecycle、viewLifecycleOwner | 何时开始/停止收集 | 这是 AndroidX Lifecycle 行为;视图用 viewLifecycleOwner,不是 Fragment 本身 |
suspend、默认参数、扩展函数与 SAM 转换是 Kotlin 语言/编译器提供的表达能力;它们不会自动获得 Android 生命周期。lifecycleScope、viewModelScope 和 repeatOnLifecycle 是 AndroidX 或相关库把 coroutine scope 与 Android owner 接起来的帮助器,必须按依赖版本和 owner 选择。
Java 调 Kotlin:先设计 Java 看得到的形状
Kotlin 默认参数对 Kotlin 调用端很舒服,但 Java 没有"省略参数"的语言能力。编译器也不会因为看见 Kotlin 默认值,就自动给 Java 生成全部重载。@JvmOverloads 适合少量参数且默认值从右侧连续的 API;跨模块公共 API 则常常更适合手写稳定重载或 Java facade。
kotlin
class ProfileFacade private constructor(
private val repository: ProfileRepository
) {
@JvmOverloads
fun load(id: String, forceRefresh: Boolean = false): Profile? {
return repository.find(id, forceRefresh)
}
companion object {
@JvmStatic
fun create(repository: ProfileRepository): ProfileFacade {
return ProfileFacade(repository)
}
}
}
java
ProfileFacade facade = ProfileFacade.create(repository);
Profile profile = facade.load("42");
Profile fresh = facade.load("42", true);
@JvmStatic 让 create 生成 Java 更熟悉的静态入口;没有它,Java 通常要经由 ProfileFacade.Companion.create(...) 调用。@JvmOverloads 让 Java 看见 load(String) 与 load(String, boolean)。这是一种互操作设计选择,并非 Kotlin 业务代码都该加注解。
SAM 转换也有方向差异:Kotlin 调 Java 的单抽象方法接口时可以直接传 lambda,例如 view.setOnClickListener { save() };Java 调 Kotlin 的普通函数类型参数时,调用形状较难读。对 Java 入口优先声明一个 Java 可见的 fun interface 或已有 Java listener,而不要把 (Result) -> Unit 当成唯一公共契约。
kotlin
fun interface ProfileListener {
fun onResult(profile: Profile?)
}
class ProfileEvents {
fun loadAsync(id: String, listener: ProfileListener) {
// 由 facade 调用实际异步实现
}
}
集合也不是简单替换关键字:Kotlin 的 List<T> 是只读接口视图,不保证底层对象绝对不可变;MutableList<T> 才暴露修改操作。跨 Java 边界时,Java 仍可能修改同一个底层 List。对外返回快照、只读模型或不可变实现,并明确所有权,能比只改类型名更可靠。泛型可变性、原始类型和 Java null 都应在 facade 处收敛。
扩展函数同样是边界问题。fun Context.toast(...) 在 Kotlin 中像成员调用,本质上是编译生成的静态方法,不能覆写真实成员,也不能访问接收者 private 成员。它适合 Kotlin 内部便利层;若 Java 也要调用,提供命名正常的 facade 或 @JvmStatic 工具入口,而不是把扩展函数误当成 Java 的新增成员。
从回调走到 suspend 和 Flow
将 callback 包成 suspend 的核心不只是"回调里 resume":需要防止回调重复、请求先完成后取消、取消后仍回调,以及取消时忘记释放监听器。isActive 后再 resume 不是原子操作,两个并发 callback 都可能观察到 active。下面的教学用简化实现用一个原子终态门禁,让成功、失败、取消三者只有一个胜者。
#mermaid-svg-gfdQKcqvC8rGmaWY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gfdQKcqvC8rGmaWY .error-icon{fill:#552222;}#mermaid-svg-gfdQKcqvC8rGmaWY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gfdQKcqvC8rGmaWY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gfdQKcqvC8rGmaWY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gfdQKcqvC8rGmaWY .marker.cross{stroke:#333333;}#mermaid-svg-gfdQKcqvC8rGmaWY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gfdQKcqvC8rGmaWY p{margin:0;}#mermaid-svg-gfdQKcqvC8rGmaWY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gfdQKcqvC8rGmaWY .cluster-label text{fill:#333;}#mermaid-svg-gfdQKcqvC8rGmaWY .cluster-label span{color:#333;}#mermaid-svg-gfdQKcqvC8rGmaWY .cluster-label span p{background-color:transparent;}#mermaid-svg-gfdQKcqvC8rGmaWY .label text,#mermaid-svg-gfdQKcqvC8rGmaWY span{fill:#333;color:#333;}#mermaid-svg-gfdQKcqvC8rGmaWY .node rect,#mermaid-svg-gfdQKcqvC8rGmaWY .node circle,#mermaid-svg-gfdQKcqvC8rGmaWY .node ellipse,#mermaid-svg-gfdQKcqvC8rGmaWY .node polygon,#mermaid-svg-gfdQKcqvC8rGmaWY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gfdQKcqvC8rGmaWY .rough-node .label text,#mermaid-svg-gfdQKcqvC8rGmaWY .node .label text,#mermaid-svg-gfdQKcqvC8rGmaWY .image-shape .label,#mermaid-svg-gfdQKcqvC8rGmaWY .icon-shape .label{text-anchor:middle;}#mermaid-svg-gfdQKcqvC8rGmaWY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gfdQKcqvC8rGmaWY .rough-node .label,#mermaid-svg-gfdQKcqvC8rGmaWY .node .label,#mermaid-svg-gfdQKcqvC8rGmaWY .image-shape .label,#mermaid-svg-gfdQKcqvC8rGmaWY .icon-shape .label{text-align:center;}#mermaid-svg-gfdQKcqvC8rGmaWY .node.clickable{cursor:pointer;}#mermaid-svg-gfdQKcqvC8rGmaWY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gfdQKcqvC8rGmaWY .arrowheadPath{fill:#333333;}#mermaid-svg-gfdQKcqvC8rGmaWY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gfdQKcqvC8rGmaWY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gfdQKcqvC8rGmaWY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gfdQKcqvC8rGmaWY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gfdQKcqvC8rGmaWY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gfdQKcqvC8rGmaWY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gfdQKcqvC8rGmaWY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gfdQKcqvC8rGmaWY .cluster text{fill:#333;}#mermaid-svg-gfdQKcqvC8rGmaWY .cluster span{color:#333;}#mermaid-svg-gfdQKcqvC8rGmaWY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gfdQKcqvC8rGmaWY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gfdQKcqvC8rGmaWY rect.text{fill:none;stroke-width:0;}#mermaid-svg-gfdQKcqvC8rGmaWY .icon-shape,#mermaid-svg-gfdQKcqvC8rGmaWY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gfdQKcqvC8rGmaWY .icon-shape p,#mermaid-svg-gfdQKcqvC8rGmaWY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gfdQKcqvC8rGmaWY .icon-shape .label rect,#mermaid-svg-gfdQKcqvC8rGmaWY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gfdQKcqvC8rGmaWY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gfdQKcqvC8rGmaWY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gfdQKcqvC8rGmaWY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ViewModel 协程
Callback Suspend Adapter
Java Callback API
成功或失败回调
恢复或失败 Continuation
协程取消注销回调
更新 StateFlow
生命周期内收集
谁创建:ViewModel 的受控协程调用 adapter。谁持有:adapter 在挂起期间持有回调注册句柄,CancellableContinuation 由协程运行时管理。何时触发:Java API 的成功、失败或协程取消。结果交给谁:成功值恢复调用协程,再由 ViewModel 更新 StateFlow;取消路径注销旧 API 回调,不能把已离开页面的回调继续送到 UI。
kotlin
// 简化示例:接口与线程策略应按现有 Java SDK 的实际契约调整。
import java.util.concurrent.atomic.AtomicBoolean
suspend fun LegacyUserApi.awaitUser(id: String): User? =
suspendCancellableCoroutine { continuation ->
val terminal = AtomicBoolean(false)
val registration = loadUser(id, object : UserCallback {
override fun onSuccess(value: User?) {
if (terminal.compareAndSet(false, true)) {
continuation.resume(value)
}
}
override fun onFailure(error: Throwable) {
if (terminal.compareAndSet(false, true)) {
continuation.resumeWithException(error)
}
}
})
continuation.invokeOnCancellation {
if (terminal.compareAndSet(false, true)) {
registration.cancel()
}
}
}
compareAndSet(false, true) 是这段示例的单次终态门禁:第一个成功或失败 callback 才能调用一次 resume,重复或并发的后续 callback 会因门禁已关闭而被忽略;若取消先赢得门禁,就调用 registration.cancel(),之后迟到 callback 也被忽略。callback 与取消同时发生时,CancellableContinuation 负责它们之间的安全竞争与 prompt cancellation 保证;调用方仍会收到并传播 CancellationException,不会把它转成 UI error。
这段代码故意省略了 dispatcher 策略、完整的 structured-concurrency scope 所有权、Java exception mapping,以及 Flow 的背压和配置选择;它不是可直接复制到生产环境的万能适配器。它还假设 Registration.cancel() 可安全地解除旧 callback,并且旧 API 的终态回调契约与这里的 one-shot gate 相容。特别是旧 Java API 的 cancel() 是否真的阻止底层 I/O、是否仍会投递一个末尾回调,需要从其源码或契约确认并测试;若 API 会同步重入、需要显式释放 listener,或 registration 的发布本身会与取消竞争,生产适配器应使用覆盖 registration 发布、取消和回调的完整原子状态机或单次投递契约。
在 ViewModel 中,挂起结果可转换为状态,而连续值才适合用 Flow:
kotlin
class ProfileViewModel(
private val api: LegacyUserApi
) : ViewModel() {
private val _state = MutableStateFlow<ProfileState>(ProfileState.Idle)
val state: StateFlow<ProfileState> = _state
fun refresh(id: String) = viewModelScope.launch {
_state.value = ProfileState.Loading
try {
_state.value = ProfileState.Content(api.awaitUser(id))
} catch (cancelled: CancellationException) {
throw cancelled
} catch (failure: Throwable) {
_state.value = ProfileState.Error(failure)
}
}
}
取消不是 UI 失败状态。CancellationException 必须原样重新抛出,让 viewModelScope、repeatOnLifecycle 或上层 structured concurrency 继续处理取消;只有非取消异常才会映射为 ProfileState.Error。因此不要在这条边界用未过滤的 runCatching、fold 或 getOrElse 把取消吞成错误页面。
viewModelScope 表达"这份工作归 ViewModel 所有";它不是把任意长期任务都变安全的开关。不要用 GlobalScope 逃避 owner 选择。若工作必须比页面或 ViewModel 更长,应由明确的应用级任务、WorkManager 或业务作用域承担,并定义可观察的结果交付方式。
Fragment 收集 Flow 时,生命周期绑定要再精确一层:View 存在期间才渲染 View。
kotlin
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
render(state)
}
}
}
这里 Flow 是 Kotlin 协程库的流抽象;repeatOnLifecycle 是 AndroidX Lifecycle 的收集边界。它会在到达 STARTED 时启动内部收集、离开时取消,并在下一次进入时重新收集。不要在没有 owner 的单例、随手创建的 scope,或 Fragment 的 view 已销毁后继续收集来更新 View。
常见误区与替代动作
| 误区 | 为什么危险 | 更稳妥的动作 |
|---|---|---|
| 一次转换每个 Java 文件 | 编译错误、行为差异和回归无法定位 | 先选一条入口链,保留可回滚的小批次 |
| Kotlin 默认参数直接暴露给 Java | Java 看不到预期重载 | 为 Java API 用 @JvmOverloads、显式重载或 facade |
| 信任平台类型 | Java 运行时仍可能返回 null |
入口即转成 T?、领域错误或默认值 |
用 GlobalScope |
任务没有可解释的取消 owner | 使用 viewModelScope、生命周期 scope 或明确业务 scope |
| 认为协程取消会自动取消旧 callback | 旧 API 仍可能持有监听器或继续 I/O | 保存注册句柄,在 invokeOnCancellation 注销并测试 |
| 在生命周期外 collect Flow | 已销毁 View 仍可能收到状态 | 通过 repeatOnLifecycle 与正确 LifecycleOwner 收集 |
| 直接替换稳定 Java API | Java 调用形状、二进制签名或异常语义可能变化 | 保留兼容 facade,评估二进制兼容与迁移窗口 |
可适配练习:给一个 Java callback API 加 Kotlin facade
选择项目中一个类似下面的旧接口,不改变它的 Java 调用方:
java
interface UserCallback {
void onSuccess(User value);
void onFailure(Throwable error);
}
Registration loadUser(String id, UserCallback callback);
练习目标是新建 Kotlin facade,而不是重写 SDK:
- 将 Java 返回值在 facade 入口归一化为
User?或显式失败状态。 - 用
suspendCancellableCoroutine包装loadUser,并在取消时调用Registration.cancel()。 - 在 ViewModel 的受控 scope 中调用 facade,把 Loading、Content、Error 放进
StateFlow。 - 在 Fragment 的
viewLifecycleOwner.repeatOnLifecycle内收集。 - 添加测试:Java API 回调
null的结果、协程取消后确实调用cancel()、取消后迟到回调不会更新状态、并发或重复的 success/failure 只有第一个终态恢复一次。
测试要基于旧 API 的真实线程与回调契约调整。若 Java API 允许同步回调、重复回调或先回调再返回 registration,adapter 的状态机需要更严格,不能只照抄教学片段。
迁移检查清单
- 这次只迁移一条可追踪的 Java 到 Kotlin 调用链,而不是全模块替换。
- 面向 Java 的 Kotlin 公共 API 已检查
@JvmStatic、@JvmOverloads、SAM 与二进制签名。 - 每个 Java 平台类型已在入口收敛为空值、默认值或显式错误。
- callback adapter 有单次终态门禁与取消注销,且迟到或重复回调不会更新失效状态。
- 协程具有明确 owner,没有用
GlobalScope绕过生命周期。 - Flow 由状态 owner 暴露,并在 AndroidX 的正确 LifecycleOwner 内收集。
- Java 异常语义、集合所有权、默认参数和扩展函数边界都有兼容测试。
面试表达
"我不会把 Java 文件一键转 Kotlin。当 Kotlin API 仍要被 Java 调用时,我先设计 Java 看到的静态入口、重载和 listener;Kotlin 调 Java 时把平台类型收敛在边界。旧 callback 用可取消的 suspend adapter 包装,ViewModel 用受控 scope 更新 Flow,Fragment 通过 repeatOnLifecycle 收集。这样语言特性、协程取消和 Android 生命周期分别有清楚的责任边界。"
系列完成后的学习计划
完成本篇后,可以按三周巩固:第一周挑一个纯工具类或 model 写 Java/Kotlin 互调测试;第二周迁移一条 callback 到 suspend 的 Repository 链并覆盖取消与 null;第三周把一个页面状态改为 StateFlow,核对 ViewModel owner、viewLifecycleOwner 收集和配置变更后的行为。下一步再阅读协程、Flow、Lifecycle 与具体 SDK 的源码契约,而不是把本篇的简化类名当作稳定公共 API。