实战:把一个现有的Compose项目转化为CMP项目
通过前面两篇文章的学习,我们已经对CMP有了一定的了解,接下来要进入实战阶段。在现实的世界中极小数项目会从0开始,今天重点研究一下如何把一个现成的用Jetpack Compose开发的Android项目转成CMP项目。
总体思路
在前面的文章Compose大前端从上车到起飞里面我们学习到了,CMP对Android开发同学是相当友好的,CMP项目与Android项目在项目结构上面是非常相似的。并且因为CMP的开发IDE就是Android Studio,因此,可以直接把一个Android项目改造成为CMP项目,而不是创建一个全新的CMP项目之后把项目代码移动进去。
具体的步骤如下:
- 添加CMP的插件,添加源码集合,配置CMP的依赖
- 把代码从「androidMain」移动到「commonMain」中去
- 把资源转换成为CMP方式
- 添加并适配其他平台
小贴士: 针对 不同的类型的任务需要采取 不同的策略,比如开发功能的时候使用「自上而下」的方式要更为好一些,因为先关注大粒度的组件,类与方法,不被细节拖住,更有利于我们看清架构和优先解决掉重点问题;但当做移植任务时,应该采用「自下而上」,因为依赖是一层套一层,先把下面的移好,上面的自然就会更加容易。
这里选用的项目是先前用纯Jetpack Compose开发的一款天气应用,项目比较简单,依赖不多,完全是用Jetpack Compose实现的UI,也符合现代应用开发架构原则,非常适合当作案例。
注意: 其实这里的项目并没有严格要求,只要是一个能运行的Android项目即可,其他的(是不是Jetpack Compose实现的,用的是不是Kotlin)并不是最关键的。因为CMP项目对于每个源码集合本身并没有明确的要求,前面的文章也讲了,每个平台的源码集合,其实就是其平台的完整的项目。移植的目的就是把 可共用共享 的代码从现有项目中抽出来放进「commonMain」中,即可以是原有的业务逻辑,也可以是新开发的代码。采用新技术或者新工具的一个非常重要的原则 就是要循序渐进,不搞一刀切。如果时间不充裕,完全可以新功能和新代码先用CMP方式开发,老代码暂且不动它,待日后慢慢再移植。当然了,纯Jetpack Compose实现的项目移植过程会相对容易一些。
下面我们进行详细的一步一步的实践。
配置CMP的插件,源码集合和依赖
首先要做的是配置Gradle构建插件(这是把Gradle常用的Tasks等打包成为一个构建 插件,是编译过程中使用的):
- 使用Kotlin Multiplatform(「org.jetbrains.kotlin.multiplatform」)替换Kotlin Android(「org.jetbrains.kotlin.android」),这个主要是Kotlin语言的东西,版本号就是Kotlin的版本号,注意要与其他(如KSP,如Coroutines)版本进行匹配;
- 添加Compose compiler(「org.jetbrains.kotlin.plugin.compose」)的插件,版本号要与Kotlin版本号保持一致;
- 以及添加Compose Multiplatform(org.jetbrains.compose」)插件,版本号是CMP的版本号。
注意,构建插件配置是修改项目根目录的那个build.gradle.kts:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.1.4" apply false
id("com.android.library") version "8.1.4" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.0.21" apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.28" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
id("org.jetbrains.compose") version "1.7.0" apply false
}
之后是修改module的build.gradle.kts,先是启用需要的插件,然后是添加kotlin相关的配置(即DSL kotlin {...}),在其中指定需要编译的目标,源码集合以及其依赖,具体的可以仿照着CMP的demo去照抄就好了。对于依赖,可以把其都从顶层DSL dependencies中移动到androidMain.dependencies里面,如果有无法移动的就先放在原来的位置,暂不动它,最终build.gradle.kts会是酱紫:
plugins {
id("com.android.application")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.compose")
}
kotlin {
androidTarget {
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
sourceSets {
androidMain.dependencies {
// Jetpack
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.activity:activity-compose:1.9.3")
val lifecycleVersion = "2.8.7"
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion")
val navVersion = "2.8.4"
implementation("androidx.navigation:navigation-runtime-ktx:$navVersion")
implementation("androidx.navigation:navigation-compose:$navVersion")
implementation("androidx.datastore:datastore-preferences:1.1.1")
// Google Play Services
implementation("com.google.android.gms:play-services-location:21.3.0")
// Compose
implementation(compose.preview)
implementation(project.dependencies.platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material")
// Network
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// Accompanist
implementation("com.google.accompanist:accompanist-permissions:0.32.0")
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
val lifecycleVersion = "2.8.3"
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion")
implementation("org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion")
}
}
}
android { ... }
dependencies { ... }
最后,把DSL android {...}中不支持的字段删除掉即可,如kotlinOptions,它用来指定Kotlin JVM target的,现改在DSL kotlin中的androidTarget()中指定了,但要注意Kotlin的JVM target要与android中的compileOptions的sourceCompatibility以及targetCompatibility版本保持一致,比如都是17或者都是11,否则会有编译错误。
需要特别注意的是DSL kotlin中的源码集合名字要与真实的目录一致,否则编译会出错。建议的方式就是依照CMP的demo那样在module中去创建androidMain和commonMain即可。另外,可以把module名字从「app」改为「composeApp」,然后把运行配置从「app」改为「androidApp」,这下就齐活儿了:
CMP的插件和依赖配置好了以后,运行「androidApp」应该就可以正常运行。因为仅是配置一些依赖,这仍是一个完整的Android应用,应该能够正常运行。这时第一步就做完了,虽然看起来貌似啥也没干,但这已经是一个CMP项目了,基础打好了,可以大步向前了。
小贴士: 通过配置依赖可以发现,CMP的artifact依赖都是以org.jetbrans.*开头的,哪怕是对于Compose本身,纯Android上面Jetpack Compose的依赖是「"androidx.compose.ui:ui"」,而CMP中的则是「"org.jetbrains.compose.ui:ui"」。虽然都是Jetpack Compose,代码是兼容的,但技术上来讲是两个不同的实现。确切地说JetBrains的Compose是从谷歌的上面fork出来的一个分支,以让其更好的适用于CMP,但完全兼容,标准的Compose代码都是能正常跑的。
把代码从「androidMain」移动到「commonMain」
这是最关键的一步了,也是最难啃的硬骨头,具体的难度取决于项目中使用了多少「不兼容」的库和API。Compose和Jetpack中的绝大多数库都是支持的,可以在CMP中使用,可以无缝切换,这是JetBrains和Google共同努力的结果,谷歌现在对CMP/KMP的态度非常的积极,给与「第一优先支持(First class support)」。所以对于依赖于room,navigation,material和viewmodel的代码都可以直接移到common中。
也就是说对于data部分,model部分以及domain部分(即view models)都可以直接先移到common中,因为这些层,从架构角度来说都属于业务逻辑,都应该是平台独立的,它们的主要依赖应该是Jetpack以及三方的库,这些库大多也都可以直接跨平台。
当然,不可能这么顺利,因为或多或少会用到与平台强相关的API,比如最为常见的就是上下文对象(Context)以及像权限管理和硬件资源(如位置信息),这就需要用到平台定制机制(即expect/actual)来进行定制。
可能有同学会很奇怪,为啥UI层还不移动到common中,UI是用Compose写的啊,而Compose是可以直接在CMP上跑的啊。Compose写的UI确实可以直接跑,但UI必然会用到资源,必须 先把资源从android中移到common中,否则UI是跑不起来的。
把资源转化成为CMP方式
在前一篇文章Compose大前端从上车到起飞有讲过CMP用一个库resources来专门处理资源,规则与Android开发管理资源的方式很像,所以可以把UI用到的资源移动到common中的composeResources里面,就差不多了。
但需要特别注意,不要把全部的资源都从androidMain中移出,只需要把UI层用到的那部分资源移出即可。androidMain中至少要把Android强相关的资源留下,如应用的icon,应用的名字,以及一些关键的需要在manifest中使用的xml等。这是因为这些资源是需要在Android应用的配置文件AndroidManifest中使用的,所以必须还放在android源码集中。
资源文件移动好后,就可以把UI移动到common中了,最后一步就是使用CMP的资源类Res代替Android的资源类R即可。
到此,就完成了从Android项目到CMP项目的转变。
添加并适配其他平台
前面的工作做好后,再适配其他的平台就非常容易了,添加其他平台的target和入口(可以仿照CMP的demo),然后实现相关的expect接口即可。由此,一个大前端 项目就彻底大功告成了。
总结
CMP对项目结构中源码 集合 的限制 并不多,每个平台相关的sourceSet可以保持其原来的样子,这对现有项目是非常友好的,可以让现有的项目轻松的转成为CMP项目,这也是CMP最大的一个优势。
References
- Jetpack Compose to Compose Multiplatform: Transition Guide
- How to convert Kotlin project to Kotlin Multiplatform Mobile after the project completion?
- From Android to Multiplatform: Migrating real 100% Jetpack Compose App to fully Multiplatform App. Intro
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!
来源:juejin.cn/post/7441956051438682138