Java 转 Kotlin 的 Android 迁移路线

一个真实的 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 转挂起 | suspendCancellableCoroutineCancellableContinuation、one-shot gate | 谁登记回调,谁解除登记 | 适配器用单次终态门禁,取消时注销 |

| 协程取消 | CoroutineScopeJobviewModelScope | 谁拥有并终止请求 | 让 ViewModel 或明确 use-case scope 拥有 Job,禁止 GlobalScope |

| Flow 收集 | Flow.collectStateFlow | 最新状态交给谁 | ViewModel 暴露流,UI 层消费流 |

| 生命周期边界 | repeatOnLifecycleviewLifecycleOwner | 何时开始/停止收集 | 这是 AndroidX Lifecycle 行为;视图用 viewLifecycleOwner,不是 Fragment 本身 |

suspend、默认参数、扩展函数与 SAM 转换是 Kotlin 语言/编译器提供的表达能力;它们不会自动获得 Android 生命周期。lifecycleScopeviewModelScoperepeatOnLifecycle 是 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);

@JvmStaticcreate 生成 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 必须原样重新抛出,让 viewModelScoperepeatOnLifecycle 或上层 structured concurrency 继续处理取消;只有非取消异常才会映射为 ProfileState.Error。因此不要在这条边界用未过滤的 runCatchingfoldgetOrElse 把取消吞成错误页面。

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:

  1. 将 Java 返回值在 facade 入口归一化为 User? 或显式失败状态。
  2. suspendCancellableCoroutine 包装 loadUser,并在取消时调用 Registration.cancel()
  3. 在 ViewModel 的受控 scope 中调用 facade,把 Loading、Content、Error 放进 StateFlow
  4. 在 Fragment 的 viewLifecycleOwner.repeatOnLifecycle 内收集。
  5. 添加测试: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。

相关推荐
计算机毕设定制辅导-无忧学长1 小时前
《基于Vue的流浪动物救助中心管理系统的设计与实现》
java·vue.js·spring boot·流浪动物救助中心管理系统
Mikko71 小时前
jackson-databind 升到 2.21.6 就安全了吗?jackson-core 是另一个坐标,它那条 high 全局库至今没收
java·后端·安全·json
Java_AI工程师1 小时前
90%的人写Function Calling,只写了"把参数传给工具执行"这一步,剩下的参数校验、错误重试、超时控制、结果格式化,全是空白。
java·人工智能·程序员
斯维赤1 小时前
LangChain4j 入门教学(Java 后端狂喜版
java·后端
Java_2017_csdn1 小时前
Java 8 Stream API 中的 map 和 flatMap 详解
java
AI深栈1 小时前
第 15 章 · 第一个 AI 工作流:Hello Graph 从 START 跑到 END
java·人工智能
搜狐技术产品小编20231 小时前
解锁Kotlin Serialization高阶玩法:详解4种自定义序列化器与动态上下文策略
java·人工智能
花间相见2 小时前
【计算基础|网络04】—— HTTP接口实战(下):接口测试、鉴权与跨域排错
java·linux·人工智能·后端·python·计算机网络·postman