Jetpack 库的版本碎片化,不同库的版本怎么对齐

Jetpack 库的版本碎片化,不同库的版本怎么对齐

Jetpack 库的版本碎片化,不同库的版本怎么对齐


Jetpack 库的版本碎片化,不同库的版本怎么对齐


从一次 gradle 报错说起


去年十月,我把一个老项目的 compileSdk 从 33 升到 34,Gradle sync 直接炸了。报错信息很眼熟:androidx.lifecycle:lifecycle-runtime-ktx:2.7.0androidx.activity:activity-ktx:1.8.0LifecycleOwner 这个接口上起了冲突。具体来说是 activity-ktx 1.8.0 依赖的 lifecycle-runtime 版本是 2.6.1,而我显式声明的 lifecycle-runtime-ktx 是 2.7.0,两个版本的 LifecycleOwner 里方法的 default implementation 变了。


这不算啥稀奇事,Android 开发者谁没被 Duplicate class 或者 Incompatible class change 恶心过。但这次我盯着报错看了十分钟,突然意识到一个问题:Jetpack 这些库的版本号,到底他妈是怎么管理的?


lifecycle 2.7.0,activity 1.8.0,fragment 1.6.2,navigation 2.7.5,room 2.6.1,workmanager 2.9.0,paging 3.2.1。这些数字放在一起,像是一堆各自为政的小王国,每个库有自己的 release cycle,有自己的 major/minor/patch 语义,有自己的 Kotlin 版本要求,有自己的最低 compileSdk。Google 在文档里轻飘飘一句 "建议使用最新的稳定版本",但"最新"是什么?同一天发布的库,有的要求 Kotlin 1.9,有的还没适配 1.8;有的要求 compileSdk 34,有的 33 就行。


我翻了一下 AndroidX 的版本发布记录,2023 年 11 月那一波 release 里,lifecycle 2.7.0 和 activity 1.8.0 确实是同一个月发布的,但 activity 没跟上 lifecycle 的最新版本。这种"同月发布但不同步"的操作,比完全不管版本对齐还让人崩溃——它给了你一个"应该能配起来"的幻觉。


BOM 不是银弹,甚至不是合格的子弹


Google 的"解决方案"是 BOM(Bill of Materials),androidx.compose:compose-bom 大概是开发者最熟悉的。2023 年 2 月发布的 BOM 机制,本质上就是 Maven 的 dependency management,让你 import 一个 BOM platform,底下所有库的版本由它统一管理。


听起来很美。实际用下来,Compose BOM 的问题在于它只覆盖 Compose 相关的库。compose.uicompose.foundationcompose.materialcompose.material3 这些确实在 BOM 里,但你的项目不可能只有 Compose。navigation-compose 在不在 BOM 里?hilt-navigation-compose 呢?paging-compose 呢?accompanist 呢?


我查了一下 androidx.compose:compose-bom:2024.02.00 的 POM 文件,里面确实没有 navigation-composenavigation-compose 的版本你得自己去 androidx.navigation:navigation-compose 里找,而 navigation 库的最新版本是 2.7.7,它依赖的 lifecycle 版本可能和 BOM 里锁定的 lifecycle 版本不一致。BOM 没有覆盖 navigationnavigation 自己又依赖 lifecycle,这个传递依赖的版本解析权交给了 Gradle 的 dependency resolution,结果就是你可能拿到一个 BOM 没管、但确实冲突了的版本。


更讽刺的是,Google 自己也在搞多个 BOM。Compose 有 BOM,但 androidx 整体没有统一的 BOM。你想对齐 roomworkmanagerpaging 和 Compose 的版本?抱歉,没有 androidx:bom 这种东西。每个库各自为政,BOM 只是把 Compose 这个小圈子里的版本对齐了一下,出了这个圈子,依然是丛林法则。


我见过最离谱的项目,gradle 文件里同时存在 composeBom = "2024.02.00"lifecycle = "2.7.0"activity = "1.8.2"fragment = "1.6.2"navigation = "2.7.6"room = "2.6.1"hilt = "2.50"。开发者试图手动管理一切,因为 BOM 覆盖不全;但手动管理的结果就是某个深夜,一个传递依赖升级了,CI 挂了,本地复现不了,因为 Gradle cache 里还存着上周的 resolved 版本。


版本号语义里的陷阱


AndroidX 库的版本号遵循 SemVer 吗?表面上是的,major.minor.patch。但"破坏兼容性"的定义在 Android 这个语境下特别模糊。


activity-ktx 1.8.0ComponentActivity 的构造函数改了,加了 R.id.content 的 fragment container 自动管理。这是 minor 版本升级,按 SemVer 应该是向后兼容的。但如果你之前在自己的 ComponentActivity 子类里手动操作了 R.id.content 的内容,升级后直接崩溃。这算不算 breaking change?Google 可能觉得"我们没改 public API 的签名",但实际行为变了。


fragment 1.6.0 引入了 FragmentManagerstrict mode,在 debug build 里检查 fragment 的非法操作。minor 版本升级,你的 debug build 可能直接抛 IllegalStateException。生产环境没事,但 CI 的 debug 测试挂了。这种"行为改变不算 breaking change"的玩法,让 minor 版本升级也变得不敢轻举妄动。


lifecycle 2.6.0LifecycleRegistry 的内部状态机重写了,修复了一些 edge case 下的状态不一致问题。但有人报告升级后 LifecycleEventObserver 的回调顺序变了,原来依赖特定顺序的代码挂了。Google 的 issue tracker 里 #251 有人吵了三十多楼,最后官方回复说"之前的顺序是 undefined behavior,现在才是正确的"。这种"修复 bug 但破坏依赖 bug 的代码"的场景,在 AndroidX 里反复出现。


我越来越觉得,AndroidX 的版本号不是给用户看的,是给 Google 内部 release 流程看的。某个库攒够了 feature 就升 minor,修了点 bug 就升 patch,major 版本常年不动是因为"我们不想做大的 branding"。activity 库从 2019 年的 1.0.0 到现在 1.9.0,五年了没升 major,但内部实现早就面目全非。


Kotlin 版本的地雷阵


比库版本更头疼的是 Kotlin 版本的对齐。AndroidX 库是用 Kotlin 写的,很多库还用了 KSP、KAPT 或者 Compose compiler。这些插件和库对 Kotlin 版本有严格的要求。


Compose Compiler 的版本必须和 Kotlin 版本严格匹配。Kotlin 1.9.0 对应 Compose Compiler 1.5.0,Kotlin 1.9.10 对应 1.5.1,Kotlin 1.9.20 对应 1.5.4……这个映射关系没有自动化工具帮你检查,升级 Kotlin 的时候如果忘了同步 Compose Compiler,编译错误信息极其晦涩,通常是 IrLinkageError 或者 ComposeInternalError,你得凭经验猜是版本不匹配。


KSP 的版本也要对齐 Kotlin。ksp-1.9.20-1.0.14 这个版本号里,1.9.20 是 Kotlin 版本,1.0.14 是 KSP 自己的版本。Room 2.6.0 开始默认用 KSP 替代 KAPT,但 KSP 的版本你又得手动管理。room = 2.6.1kotlin = 1.9.20ksp = 1.9.20-1.0.14composeCompiler = 1.5.4,这四个版本号之间的兼容性,没有任何一个 BOM 能帮你搞定。


我见过有团队在 gradle 里写了个 kotlinBom 的自定义 extension,试图把 Kotlin、KSP、Compose Compiler、serialization 的版本统一管理。但这不是官方方案,是团队自己造的轮子,每个新人来了都要学习这套"家规"。


Google 在 2023 年的 Android Dev Summit 上提过一嘴"正在考虑 Kotlin 相关的 BOM",但到现在(2024 年初)没有任何实质进展。也许他们觉得 Compose BOM 已经够用了,剩下的让用户自己拼。


传递依赖的黑暗森林


Gradle 的 dependency resolution 默认是" newest wins "。你有 lifecycle 2.7.0 的显式声明,某个库传递依赖了 lifecycle 2.6.1,最终 resolved 版本是 2.7.0。这听起来合理,直到你遇到 API 不兼容。


lifecycle 2.7.0 加了 Lifecycle.repeatOnLifecycle 的新 overload,或者改了某个 internal 类的包名。传递依赖这个库的老版本代码,在 runtime 链接到 2.7.0 的 class,可能直接 NoSuchMethodError 或者 ClassNotFoundException。Gradle 的 resolution 只保证版本号统一,不保证 API 兼容。


androidx.core:core-ktx 是重灾区。几乎每个 AndroidX 库都依赖 corecore 的版本升级非常频繁。core 1.12.0 改了 NotificationCompat 的行为,加了 Locale 相关的新 API。如果你的某个传递依赖库在 core 1.10.0 下编译,但 runtime 被 resolution 到 1.12.0,notification 相关的代码可能行为异常。


./gradlew app:dependencies 这个命令可以打印完整的 dependency tree,但一个中等项目的 tree 可能有上万行。你想找到"谁带进了这个版本的 core",得在 log 里 grep 或者写 Gradle 脚本做 custom resolution strategy。这不是开发者应该花时间的领域,但 AndroidX 的版本碎片化把这个负担甩给了用户。


有人用 resolutionStrategy.force 强行锁定所有 AndroidX 库的版本。这能避免冲突,但带来的问题是:某个库的新版本修复了崩溃 bug,但因为你的 force 策略,它永远拿不到修复。这是用稳定性换安全,或者用安全换稳定性,两头不讨好。


Google 的 release 节奏与开发者的实际节奏


AndroidX 的 release 频率是每月一次,通常在月初。这个节奏对 Google 内部可能是合理的——每个团队按自己的 milestone 发布,但对外部开发者来说,这意味着每个月都要评估一遍"这些新版本里有没有我需要的修复,有没有我要躲的坑"。


2023 年 12 月的 release 里,media3 1.2.0compose 1.6.0 同时发布。media3 要求 compileSdk 34,compose 1.6.0 的 BOM 也要求 compileSdk 34。但你的项目可能还没准备好升 compileSdk,因为 targetSdk 34 带来了前台服务类型声明、照片选择器权限模型等一堆行为变更。你只想升 media3 拿一个播放器的 bugfix,但被迫要处理整个 compileSdk 的升级。


Google 的文档里会说"建议保持所有库最新",但企业项目的升级节奏受 QA 周期、应用商店审核、用户设备分布制约。一个年活千万的 App,不可能每个月跟着 AndroidX 的版本走。现实是:团队选一个"相对稳定的版本组合",锁死几个月,只打安全补丁,feature 版本能拖就拖。


这种"锁死策略"和 AndroidX 的"每月发布"形成了结构性矛盾。Google 的工程师在 I/O 上演示的永远是"最新版本的最佳实践",但台下坐着的开发者心里想的是"我能不能不动"。


Compose 的 versioning 尤其让人疲惫。material3 从 1.0 到 1.1 到 1.2,每个 minor 版本都有视觉规范的调整。Button 的 default padding 变了,TopAppBar 的高度变了,ColorScheme 的 seed color 算法变了。这些不是 breaking API change,但你的 UI 测试截图对比会失败,design team 会提 bug,产品经理会问"为什么按钮变大了"。升一个 minor 版本,可能要过一轮设计验收。


社区里的土办法与官方的尴尬


面对这种碎片化,社区里滋生出各种土办法。有人写 Gradle plugin 自动扫描所有 AndroidX 依赖,生成一个"推荐版本矩阵"。有人维护 GitHub repo,手工整理每个月"经过验证能配在一起"的版本组合。有人在 Reddit 上发帖问"有没有人试过 lifecycle 2.7.0 + navigation 2.7.5 + room 2.6.1 + hilt 2.50 + kotlin 1.9.22 的组合,能不能编译过"。


这些土办法的存在,本身就是官方工具链失败的证据。


Google 的 Android Studio 团队做了 Version Catalog(libs.versions.toml),试图把版本管理标准化。但 Version Catalog 只是"在哪里写版本号"的规范,不解决"版本号应该是什么"的问题。你可以把 lifecycle = "2.7.0" 写在 toml 里,也可以写 lifecycle = "2.6.2",Catalog 不告诉你哪个是对的。


Android Studio 的 Dependency Analyzer 能高亮出过时的依赖,但它不会告诉你"这个版本和另一个库有已知冲突"。它只会说"2.7.0 可升级到 2.7.1",不会说"2.7.1 和某个版本的 activity 不兼容"。


我有时候想,如果 Google 自己维护一个"已验证组合"的列表,比如"2024 年 1 月推荐栈:Kotlin 1.9.22 + KSP 1.9.22-1.0.17 + Compose BOM 2024.02.00 + lifecycle 2.7.0 + activity 1.8.2 + ...",每个月更新一次,开发者直接抄作业,能省多少事。但 Google 不会做这种事,因为这意味着官方要为特定版本的兼容性背书,而 AndroidX 的 release 模型是"每个库独立演进,组合兼容性由 Gradle 保证"。


问题是 Gradle 保证不了。Gradle 能保证的是"同一个 artifact 只有一个版本在 classpath 里",不能保证"这个版本和另一个库的所有 API 在语义上兼容"。AndroidX 的库之间不是松耦合的独立服务,它们是共享 LifecycleViewModelSavedState 这些核心抽象的深度纠缠体。lifecycle 的一个 internal 改动,可能通过 activity 的传递依赖影响到 fragment,再影响到 navigation,再影响到你的 Compose 代码。


一个具体的崩溃案例


去年我处理过一个线上崩溃,堆栈长这样:


java.lang.NoSuchMethodError: 'void androidx.lifecycle.LifecycleRegistry.<init>(androidx.lifecycle.LifecycleOwner)'
    at androidx.activity.ComponentActivity.<init>(ComponentActivity.java:...)
    at com.example.MyActivity.<init>(MyActivity.kt:...)

LifecycleRegistry 的构造函数在 lifecycle 2.5.0 还是单参数,到 lifecycle 2.6.0 改成了双参数,加了 boolean enforceMainThreadactivity 1.7.0 开始依赖新版本的 lifecycle,但我们的项目里有个第三方 SDK(某个广告联盟的 aar),它内部打包了旧版本的 lifecycle class。


Gradle 的 dependency resolution 对 aar 里打包的 class 无能为力。aar 是 fat 的,它把 lifecycle 的 class 文件直接塞进了自己的 classes.jar。runtime 类加载的时候,aar 里的旧版本 LifecycleRegistry 先被加载到,然后 activity 1.7.0 的代码试图调用新版本的构造函数,boom。


这个 case 的 root cause 是第三方 SDK 的 fat aar 问题,但触发条件是 AndroidX 的版本升级。如果 lifecycle 的 API 没有变,或者 activity 没有升级依赖,这个崩溃不会发生。AndroidX 的版本碎片化,让"升级一个库"的决策变得异常复杂——你不知道哪个传递依赖、哪个第三方 SDK、哪个 classpath 加载顺序会把问题引爆。


我们最后的 workaround 是强制把那个广告 SDK 的版本回退到打包旧 lifecycle 之前的版本,同时锁死 activity 不超过 1.6.0。这不是解决方案,这是妥协。但 AndroidX 的版本生态里,妥协是常态。


对 Jetpack 整体策略的质疑


Jetpack 的 branding 在 2018 年推出时,承诺的是"一套一致的、向后兼容的、经过 Google 官方维护的 Android 开发组件"。六年过去,这个承诺在"一致性"上打了很大折扣。


androidx 命名空间下的库,从十几个膨胀到一百多个。每个库有自己的 release notes,自己的 issue tracker 标签,自己的 deprecation policy。preference 库几乎 abandoned,最后一个有意义更新是 2022 年;workmanager 还在活跃演进,但 API 表面越来越大。roompaging 的跨版本迁移指南写得像考古文档,因为中间经历了 KAPT 到 KSP、Paging 2 到 Paging 3、Room 的 incremental annotation processing 的多次底层重构。


Google 在 2023 年把 androidx 的源码仓库从多个 repo 合并成了一个巨大的 monorepo(androidx/androidx),内部开发效率可能提升了。但对外部开发者来说,这个 monorepo 只是让 GitHub 上的 issue 和 PR 更难找了。你想给 lifecycle 提个 bug,要在上万条 commit 里定位到相关代码;你想看 navigation 的 release tag,要在一百多个 library group 里翻找。


Compose 的崛起加剧了碎片化。原来 View 体系的库和 Compose 体系的库并行存在,materialmaterial3navigation-fragmentnavigation-composeviewmodelhilt-navigation-viewmodel。Google 说"我们不放弃 View 体系",但资源明显在向 Compose 倾斜。material 库(View 的)last meaningful update 是什么时候?material3 每个版本都在加新组件。


这种"双线维护"让版本对齐更复杂。你用 View 的 navigation 2.7.5,和 Compose 的 navigation-compose 2.7.5,依赖的 navigation-runtime 是同一个 artifact,但 navigation-compose 额外依赖 compose.uihilt-navigation-compose。BOM 只覆盖 Compose 侧,View 侧的版本你得自己盯。


个人能做什么,以及为什么不够


作为个体开发者或者小团队,你能做的防御措施有限。


Version Catalog + BOM 能覆盖一部分,但覆盖不全。./gradlew dependencyInsight 能帮你排查具体冲突,但排查过程是 reactive 的,不是 preventive 的。CI 里跑 ./gradlew build--refresh-dependencies 能发现一些版本解析问题,但 runtime 的类加载冲突、behavioral change 的 bug,只有真机测试或者线上崩溃能暴露。


有人用 Renovate 或 Dependabot 自动提 dependency upgrade PR。这在纯后端项目里工作得很好,因为后端库的 SemVer 相对靠谱,单元测试覆盖够的话自动升级风险可控。Android 项目里自动升级?我试过,Renovate 提了一个 lifecycle 2.6.2 -> 2.7.0 的 PR,CI 过了(因为单元测试不跑 Android runtime),合并后第二天线上崩溃率涨了 0.3%。回滚,手动分析,发现是某个第三方 SDK 的传递依赖冲突。Renovate 不知道 AndroidX 的版本政治,它只知道"2.7.0 > 2.6.2"。


锁死所有版本是最安全的,也是最保守的。但锁死意味着你拿不到安全修复、性能优化、新 API。Android 14 的 behavior change 可能要求你升级某个库来适配,这个升级又牵一发而动全身。2023 年的前台服务权限变更,要求 workmanager 2.9.0+ 才能正确声明 foregroundServiceType。你不想升 workmanager?那你的后台任务在 Android 14 上可能被系统杀死。被迫升级,被迫面对版本对齐的噩梦。


最后


AndroidX 的版本碎片化不是技术问题,是组织问题。一百多个库、几十个团队、各自的 release cycle、各自的 KPI,拼凑出一个"每个月都有新版本"的繁忙景象,但"能放心组合在一起"的版本组合,要靠开发者自己在黑暗里摸索。


BOM 是个体面的尝试,但体面的尝试覆盖不了不体面的现实。Google 要么把 BOM 扩大到整个 AndroidX 生态,并且承诺组合兼容性;要么承认现状,给开发者一个"已验证组合"的参考清单。现在的状态是两头不靠:官方假装有 BOM 就够了,实际上 BOM 漏掉的地方比覆盖的地方还多。


我现在的项目里有个 gradle/androidx.versions.toml,里面三十多行版本号,每行旁边都有注释:"2024.02 验证过"、"等 Kotlin 1.9.23 再升"、"和某 SDK 冲突勿动"。这个文件是我们团队的血泪史,也是 Jetpack 版本管理的失败纪念碑。


下个月 AndroidX 又要发新版本了,我会打开 release notes,扫一眼 lifecycleactivityfragmentnavigationroom 的更新,然后大概率什么都不升。

Lint 自定义规则开发,我写完的第一条规则 2026-06-10
Square 开源库全家桶:OkHttp、Retrofit、Moshi 2026-06-10

评论区