对于 Compose,很多做 Android 开发的同学可能更了解一些。可能是因为 Compose 本身使用的是 Kotlin 开发。Compose 全称 Jetpack Compose,是 Google 推出的用于构建跨平台原生 UI 的声明式框架,其跨平台版本为 Compose Multiplatform,核心由 Google 主导发明并推动。
不过很多同学可能没了解过 Compose 在开发跨平台中的表现,也就是 Compose Multiplatform。对移动端开发来说,能掌握一门桌面开发的语言对自己在职场中的竞争肯定更有优势。所以,就让我们一起了解下 Comopse 在开发桌面程序中的一些问题吧。
现状
在移动应用的开发上,Compose 支持得比其他平台(MacOS、Windows 和 Linux 等)更加完善。而如果想用 Compose Multiplatform 实现跨平台,以现在的状况来看,对于简单的应用是足够的,但是如果想要实现复杂的功能,依然需要自己到各个平台进行功能补齐。虽然 Compose 提供了一种桥接各个平台的机制,但这依然要求开发者需要具备对应平台的开发能力。
Compose Multiplatform 在开发桌面程序方面,又要分成两种情况来分析:一种是基于 JVM 的,一种是非基于 JVM 的。
我们知道 Java 本身是一种跨平台的语言。也就是 Java 程序运行在 JRE(Java 运行时,通俗理解就是运行环境) 中。Java 就是通过 JRE 来抹平各个平台的差异,以此实现跨平台。因此,Compose Multiplatform 的基于 JVM 的桌面程序就是将我们的代码打包成 jar 文件并和 JRE 一起打包。在启动程序的时候先启动 JRE,然后再通过 JRE 运行我们的 jar 文件。非基于 JVM 的程序则是将 Kotlin 直接编译成不同平台的可真行的原生二进制文件(如 Windows 的 .exe 等)。这些文件无需依赖 JRE,可直接被操作系统识别并运行。
因此两种不同类型的桌面程序在开发的各个环节存在显著的不同。
在开发层面基于 JVM 的开发就意味着我们可以使用 Java 生态的所有库。这对我们本身就熟悉 Java 或者 Android 开发的同学来说非常友好。如果是非基于 JVM 的程序,那就意味缺少了某个库,我们就得使用对应平台的开发语言开发,然后通过 Compose Multiplatform 提供的机制进行桥接。如果我们要适配 Mac、Windows 和 Linux 三个平台,也就得为三个平台分别进行适配。
但基于 JVM 开发也有它的缺点,这主要表现在:1).启动速度慢,因为启动应用之后得先启动 JRE,然后再启动我们的程序。2).包体积比较大,因为需要把 JRE 打包进去,这会导致安装包体积比较大。
如果对包体积和启动性能没有太高的要求,那么直接使用基于 JVM 的方式开发会是比较好的选择。基于 JVM 的开发方式,适配行更好,相对更加成熟。
我在准备开发之前对 Compose Multiplatform 的适配情况做了调研(主要是针对开发 Desktop 而非 Mobile),结果如下(✅ 表示现在已有现成可用的工具、🟡 表示暂未进行调研、❌ 表示现在还没有比较成熟的可用的工具):
shell
## ✅ 开发环境
### ✅ IDE
### ✅ 构建工具
## UI
### ✅ 基础
#### ✅ 颜色
#### ✅ 内边距、外边距、背景、事件(点击/长按/双击/拖拽)
### 组件
#### ✅ 文本
##### ✅ 普通文本(大小/间距/对齐/颜色/字体/加粗等)
##### ✅ 富文本(混合/高亮)
#### ✅ 图片
##### ✅ 普通图片(内置资源 PNG SVG)
##### ✅ Material 自带 Icon
##### ✅ 网络图片、本地文件
##### ✅ Lottie
#### ✅ 按钮
##### ✅ 普通按钮
##### ✅ 单选
##### ✅ 多选
##### ✅ 开关
#### ✅ 输入框
##### ✅ 单行、多行
##### ✅ 密码
#### ✅ 进度/加载
##### ✅ 进度条
##### ✅ 加载态
#### ✅ 提示
##### ✅ 对话框(顶部/底部/中间/可拖拽)
##### ✅ 底部 SnackBar
##### ✅ 局部气泡 Popup
##### ✅ 弹出菜单
##### ❌ 消息 Toast
#### ❌ 图库
##### ✅ 图片选择
##### ❌ 图片拍摄
##### ❌ 图片浏览(多图/手势/放大)
##### ❌ 图片裁剪(头像)
#### 🟡 视频
##### 🟡 视频播放
##### 🟡 视频录制
#### ❌ 浏览器
##### ❌ 基础使用
##### ❌ JS 注入/原生交互
#### 自定义
### ✅ 布局
#### ✅ 可滚动容器
#### ✅ 基本布局方式(水平/垂直/重叠)
#### ✅ 列表容器/分页加载
#### ✅ 网格布局
#### ❌ 流式布局
### ✅ 页面(常用架构)
#### ✅ 顶部 TAB + 左右翻页
#### ✅ 顶部导航 + 左右翻页
#### ✅ 左右侧抽屉
#### ✅ 页面跳转(入参/回参)
#### ✅ 路由体系
### 🟡 效果
#### 🟡 动画
#### 🟡 渐变
#### 🟡 模糊
#### 🟡 绘制
#### 🟡 复杂交互
### ✅ 换肤
## 🟡 音视频
### 🟡 音频
### 🟡 视频
## 🟡 传感器
## ✅ 通信
### ✅ 单进程通信(EventBus/广播)
### ❌ 跨进程通信
## ✅ 时间
## ✅ 国际化
## 🟡 权限
## ✅ 网络
### ✅ 网络请求
### ✅ JSON 解析
## ✅ 持久化
### ✅ 文件形式
### ✅ 数据库形式(SQLite)
### ✅ 键值对形式
## ✅ 调试
### ✅ 日志
### ✅ 调试
## 🟡 稳定性
### 🟡 性能监控
## ✅ 架构设计
### ✅ 依赖注入
### ✅ MVVM/MVP/MVC
## ✅ 发布
上述统计截至 2025年8月,欢迎补充和完善~
上述调研也得分成是否基于 JVM 两种情况来讨论,比如说依赖注入,如果是开发基于 JVM 的,那么 Java 层面的各种依赖注入框架都是可用的,但是如果非基于 JVM,那就得通过原生层面来解决这些问题。另外就是我调研的分析的是它是否已经有现成可用的框架,也就是从开发的生态的角度考虑。
实战的问题
除了上述问题之外,在实际开发中我们还是会遇到一些其他的问题。下面我就介绍下这些问题。
混淆的问题
混淆是为了保证代码的安全性,同时也可以删减一些没有使用到的方法,减小安装包的体积。因为过往基于 Java 开发的项目大多是后端,而后端代码部署在自己的服务器,因此开发者不必重视代码的混淆。因此,很多三方的 java 库中可能会使用诸如反射之类的方法,但是这些库可能不会像 Android 的库一样,在 README 里面明确标注出如何配置混淆。因此,这会给我们开发带来一些不便。
Compose Multiplatform 的混淆和 Android 一样,使用的都是 deproguard,开启混淆的方式:
groovy
compose.desktop {
application {
buildTypes.release.proguard {
isEnabled.set(true) // 整体
obfuscate.set(true) // 混淆
configurationFiles.from(project.file("compose-desktop.pro"))
}
}
}
可以查看官方文档了解: Minification and obfuscation。这是我在项目中使用的混淆配置文件,以供参考:compose-desktop.pro.
加密的问题
根据过往 Android 开发的经验,当我们需要在代码中隐藏某些重要的逻辑的时候(如加密算法、关键业务逻辑),我们倾向于使用 JNI/NDK 的方式,将这部分代码用 C/C++ 实现,并编译为原生库。相比 Java 字节码(.class 文件)可通过反编译工具轻松还原逻辑,原生库的逆向破解(如反汇编、反编译为可读代码)难度显著更高。
Android 系统基于 Linux 内核,其原生库统一采用 ELF 格式的 .so 文件(需适配不同 CPU 架构如 arm64-v8a、armeabi-v7a 等,但文件类型单一),因此只需针对目标架构编译 .so 即可覆盖 Android 平台需求。
而在 Compose Multiplatform 桌面开发中,若需适配 Windows、macOS、Linux 三大平台,情况则更复杂:三个系统的原生库格式完全不同 ------ Windows 的 .dll 文件,MacOS 的 .dylib 文件,Linux 的 .so 文件。这意味着必须为每个目标平台单独编写 C/C++ 代码,并通过对应平台的编译器生成专属原生库。这不仅增加了开发的工作量和难度,也增加了测试的工作量。
打包的问题
Compose 桌面的原生打包功能基于 JDK 内置的 jpackage 工具,而 jpackage 有一个核心限制:只能在目标平台上生成对应平台的安装包。例如:
- 在 Windows 上才能生成 .msi 或 .exe 安装包;
- 在 macOS 上只能生成 .dmg 或 .pkg 安装包;
- 在 Linux 上只能生成 .deb 或 .rpm 安装包。
因此,在 macOS 上执行 packageReleaseMsi 这类针对 Windows 的打包命令,jpackage 会因缺少 Windows 工具链而失败。
解决这个问题的一个办法是通过 Github Action 打包,如下是我的使用的打包的脚本,这里设置了构建的 os 环境,以及 JDK 等信息,然后通过 gradlew 命令进行打包。打包完毕之后将打包的结果上传到 Github,然后,我们可以到指定的位置下载安装包。
yaml
name: Build Compose Desktop App
on:
push:
branches: [main]
pull_request:
branches: [main]
release:
types: [published]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
name: Build on ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build app
run: |
chmod +x gradlew
./gradlew packageReleaseDistributionForCurrentOS -Pbuild_version="1.0.3"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: compose-app-${{ matrix.os }}
path: composeApp/build/compose/binaries/main-release/*
采用这种打包方式就可以直接在 Github 上面打包三个平台的安装包,而无需我们在本地更换不同操作系统进行打包,要方便很多。
资源获取的问题
Compose Multiplatform 在资源的管理上借鉴了 Android 的经验,资源需要放在。composeResources 目录下面,可以使用多语言、图片等资源。
但是和 Android 不一样的地方在于,Compose Multiplatform 中获取资源的方法存在一定的局限性。以获取字符串的方法为例,它有两种类型的方法,如下所示:
kotlin
@Composable
fun stringResource(resource: StringResource): String { ... }
suspend fun getString(resource: StringResource): String { ... }
显然,一个方法使用了 @Composable 注解,这说明,我们只能在 @Composable 注解的方法中使用它。另一个方法使用了 suspend 修饰,这表明我们只能在协程范围内使用它。但是,如果说我们想要在业务逻辑方法中获取多语言并返回,也就是非 @Composable 注解修饰并且非协程方法,那么就行不通了。
构建信息的问题
在之前,不论在开发 Android 还是 iOS 应用,我都会将应用打包的时间戳等信息写入到打包文件中。因为 Android 和 iOS 默认可以将构建的 Debug/Release 或者版本号等信息写入到打包文件中,因此,我也不需要为此费心。但是 Compose Multiplatform 不仅默认没有版本号,也无法区分 Debug/Release 等信息。因此,就需要我们在打包的时候自己进行写入。这也是使用起来不太方便的地方之一。
对于版本号,我通过 Gralde 构建时的指令指定构建的版本信息。然后在 build.gralde 中读取构建的信息。如下所示:
groovy
// gradle 命令:gradlew xxx -Pbuild_version="1.0.3"
val devVersion = "1.0.0"
val version = if (project.hasProperty("build_version"))
project.findProperty("build_version")!!.toString() else devVersion
println("building app, version: $version")
nativeDistributions {
packageVersion = version
}
而构建的 Debug/Release 以及构建的时间信息则通过 build.gradle 直接指定:
groovy
buildTypes.release {
jvmArgs += listOf(
"-Denv=release",
"-Dbuild=${SimpleDateFormat("yyMMddHHmmss").format(Date())}",
"-Dbuild_version=${version}"
)
}
这样我们可以在运行时通过 System 获取上述传入的信息:
kotlin
object AppUtils {
/** 获取版本号 */
fun getAppVersionName(): String = System.getProperty("build_version") ?: "1.0.0"
/** 构建的信息 */
fun getBuild(): String = System.getProperty("build") ?: "1"
/** 获取环境信息 */
fun getEnv(): String? = System.getProperty("env")
}
UI 描述的问题
首先是预览的问题。Compose Multiplatform 提供了 @Preview 注解用来标志某个方法可以提供 UI 的预览。不过这存在一个前提条件,那就是这个方法不能带有任何参数。这不算什么大问题。我们可以通过新增一个方法专门用来预览解决这个问题,即创建一个不带任何参数大方法,并使用 @Preview 注解进行修饰。
其次是 Material 和 Material3 两套库的问题。两个库的控件存在一些区别,有时候我们不得不使用两个库,但是有些控件又在两个库中同时存在,比如 Text、Image 这样的基础控件。然而,这些控件在两个库中又存在视觉上的差异,比如 Text 在 Material 中比较纤细且无法跟随换肤,在 Material3 中比较粗且可以跟随换肤。所以,在开发过程中,我们需要注意导入的究竟是哪个版本的库。关于两者差异,参见:《在 Compose 中从 Material 2 迁移至 Material 3》。
数据存储的问题
最后是 Compose Multiplatform 的数据存储的问题。本质上这不是 Compose Multiplatform 的问题,而是桌面系统和移动应用系统对应用数据管理的问题。因为不论 Android 还是 iOS 都有一个应用私有目录的概念,也就是每个应用专属的数据存储目录。但是,桌面程序没有应用专属目录的概念。因此需要注意数据存储的位置和数据的安全性,比如键值对的存储问题。下面代码使用的是 Java 的 Preferences:
kotlin
private val prefs = Preferences.userRoot().node("user-preferences")
val settings: Settings = PreferencesSettings(prefs)
而它实际存储目录是 ~/Library/Preferences/com.apple.java.util.prefs.plist,这是一个公共目录,不同应用访问的是同一个文件。这说明如果 key 设置不当,可能会出现多个应用使用同一个 key 的情况。这也带来了数据安全的隐忧。开发的时候需要特别注意。
总结
这篇文章总结了 Compose Multiplatform 开发桌面程序的现状,以及我在实际开发过程中遇到的一些问题和解决办法。Compose Multiplatform 目前生态日趋成熟,已支撑多款千万级用户量的复杂生产级应用落地。虽部分平台存在少量适配与性能优化需求,但整体能高效开发跨 Android、iOS、桌面、Web 的多平台应用并实现高比例代码共享。
另外,在我实践中也发现 AI 对于 Compose Multiplatform 的回答并不总是准确。这可能是因为 AI 训练的数据的滞后性,所以,我们了解 Compose Multiplatform 的时候应该优先选择 官方文档。
如果你觉得这篇文章对你有帮助,欢迎点赞和转发 ❤️ ~