背景
KMP跨平台是一门新兴的技术,在此时入局未免会碰到各式各样的坑,而本系列文章将记录我使用 KMP 以来所踩的坑,作为记录,同时也帮助大家入局 KMP。
爬坑
一、K2编译器 + Parcelize + KMP 无法正常使用
背景:看到这个问题,大家肯定很疑惑,Parcelable 是Android 平台的特性,但是为什么需要在 KMP 上使用呢?
事情是这样的,Compose Multiplatform 作为一个从 Android 平台中迁移的 UI 框架,不可避免地会使用到 Android 的特性,像是 rememberSaveable
默认情况下在Android 中的实现就是将内容存到 SavableStateRegistry
中,并在 UI复原时从中读取复原。
如果我们不特地声明一个Saver
,存基础数据例如 Int、Long、String当然没有问题,但是如果是一个数据类型,会自动将它作为一个 Parcelable
对象存储。但是问题来了,common模块并没有 Parcelable
这一个概念。
那能不能声明一个 common 模块 Parcelable
,它在 Android 平台上就正常实现,其他平台就空实现呢?答案是可以的。
kotlin
// common package
expect interface CommonParcelable
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class CommonParcelize()
// android package
actual typealias CommonParcelable = android.os.Parcelable
// For parcelaze plugin
actual typealias CommonParcelize = kotlinx.parcelize.Parcelize
编写以上代码之后我们就可以对我们 common 模块的数据类使用了:
kotlin
@CommonParcelize
@Stable
data class NoteItem(
val id: Long,
val title: String,
val content: String
): CommonParcelable
// use it
var noteItems: List<NoteItem> by rememberSaveable {
mutableStateOf(emptyList())
}
就这么简单几行代码就可以正常运行。使用默认的Saver把数据类当 Parcelable 存储而不用自己手动自定义 Saver真的很方便。
但是,这种写法在 K2 编译器无法使用了。
Kotlin 编译器有两个模块,其中一个是 Kotlin 语法模块,用于理解编写的代码,另一个是编译模块,用于将 Kotlin 代码生成特定平台可执行的产物。例如 Bytecode,JavaScript,Executable,Wasm。他们也称为 frontend 和 backend。
K2 Compiler 重写了 frontend,将编译次数降为一次,使编译拥有更优的性能,然而在编译期无法访问平台特性的代码,例如平台代码的类型、注解等等。
这将直接导致了 Parcelize 插件在使用 K2 的 Kotlin Multiplatform 项目不可用,由于 Parcelize 插件是 Android 平台特供的,需要检测 Android 平台代码的注解并生成代码,而 KMP 的 common 代码是不与平台相关的,这将导致现存的 expect/actual 标识的注解将无法在 K2 compilier 上使用。
而目前Parcelize插件官方的解决方式是增加编译参数,触发平台代码检测,从而使特定注解和其使用的地方增加一次平台代码检测,使 Parcelize 插件生效。
kotlin
freeCompilerArgs.addAll("-P", "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=example.CommonParcelize")
而这种方式也是有副作用的,增加了一次平台代码检测,也增加了编译时间。这也算是编译时间换编码时间。
二、 Gradle二次混淆问题
gradle 8.5 以上的 library module 需要将 isMinifyEnabled 设置为 false,否则会造成多次混淆,即:App module将代码混淆一遍,Library 也将代码混淆一遍,最直接的现象是 release 包跑起来找不到类而闪退。
三、KMP Desktop找不到Class
在面向 Desktop 平台的产物中,我们会带一个JDK 到程序中。
一个 Hello World 程序直接达到了 200MB
而问题就出在了这个JDK,由于Gradle 无法自动检测你运行时实际上需要使用到哪些类,打进去的 JDK 是有删减的,当某个类没有打包进程序中,在运行时将会报错。
所以可以在打包前检测一下自己需要哪些类,又或者报错时通过错误信息查看哪些类丢失。在 Gradle 脚本中声明:
kotlin
nativeDistributions {
modules("java.sql")
}
当然,如果你懒,又或者你并不在意包体大小,可以直接声明把整个 JDK 打进包里,避免运行时异常。
kotlin
nativeDistributions {
includeAllModules = true
}
后记
由于新技术的的资料较少,部分内容未必准确,欢迎勘误。当然,Kotlin 迭代非常快,当你看到这篇文时,可能某些缺陷已经被修复,请以最新的为准。