【Compose Multiplatform 跨端开发学与练】第1课 从零开始
本课目标:
理解 Compose Multiplatform 的技术定位与核心价值,完成全平台开发环境搭建,创建并运行第一个跨端应用,读懂项目结构与核心代码,为后续九课打下坚实基础。
系列整体规划
本系列面向具备一定 Kotlin 基础、希望系统掌握跨端开发能力的开发者。全系列共 10 课,遵循"先跑起来,再懂原理,最后能交付"的路径设计,每课包含理论讲解、实战示例和配套习题。
课次 主题 核心内容 难度
第1课 从零开始 技术概览、环境搭建、第一个应用、代码解读 ⭐
第2课 Compose 基础语法 @Composable、状态管理、重组机制、Modifier 体系 ⭐⭐
第3课 布局与组件 Column/Row/Box、LazyColumn、Material3 组件库 ⭐⭐
第4课 导航与路由 Navigation Compose、类型安全路由、深层链接 ⭐⭐⭐
第5课 网络与数据层 Ktor 客户端、序列化、Repository 模式 ⭐⭐⭐
第6课 状态管理与架构 ViewModel、单向数据流、依赖注入 ⭐⭐⭐⭐
第7课 平台适配与互操作 expect/actual、SwiftUI 互操作、平台特定 API ⭐⭐⭐⭐
第8课 资源管理与主题 多平台资源、图片加载、深浅色主题 ⭐⭐⭐
第9课 测试与调试 Compose UI 测试、单元测试、性能分析 ⭐⭐⭐⭐
第10课 发布与部署 Android/iOS/桌面/Web 打包发布、CI/CD ⭐⭐⭐⭐⭐
学习路径说明:第1-3课打基础,第4-6课构建完整应用能力,第7-8课解决平台差异化问题,第9-10课完成工程化闭环。每课习题均包含概念理解、代码实践和综合设计三类,建议全部动手完成。
第1课 从零开始
一、为什么选择 Compose Multiplatform
1.1 一个真实的选择困境
假设你是一名 Android 开发者,产品经理拿着需求来了:三个月内,同一款应用要上线 Android、iOS、Windows/macOS 桌面端,甚至可能要做 Web 版。预算有限,团队只有三个人。
你有几个选择:
选项 A:各端独立开发。 Android 用 Kotlin + Jetpack Compose,iOS 用 Swift + SwiftUI,桌面用 C++ 或 Electron,Web 用 React。质量最高,但三个月的工期意味着你要招至少五个方向的工程师,成本直接爆炸。
选项 B:Flutter。 一套 Dart 代码全平台运行,UI 一致性强。但你现有的 Android 代码无法复用,团队需要重新学习 Dart,打包出来的应用体积偏大,且 iOS 端与原生生态的融合始终隔着一层。
选项 C:React Native。 JS 生态庞大,热更新方便。但跨端桥接的性能损耗、复杂的原生模块依赖、以及 JS 生态的碎片化,都让大型项目的维护成本陡增。
选项 D:Kotlin Multiplatform(KMP)+ Compose Multiplatform(CMP)。 你的 Kotlin 知识可以直接复用,业务逻辑和 UI 都可以共享,Android 端就是原生的 Jetpack Compose,iOS 端编译为原生 Skia 渲染,桌面端跑在 JVM 上,Web 端通过 Kotlin/Wasm 运行。渐进式采用——你可以先共享逻辑,再逐步共享 UI。
选项 D 就是我们这个系列要深入的方向。
1.2 CMP 的本质:不是替代,而是延伸
要理解 CMP,必须先理解它的母体——Kotlin Multiplatform(KMP) 。
KMP 解决的是"逻辑共享"问题:你可以把数据模型、网络请求、业务规则、状态管理这些与 UI 无关的代码写在 commonMain 中,编译时按平台生成对应的产物——Android 上是 JVM 字节码,iOS 上是 Native 二进制,Web 上是 JavaScript/Wasm。
但 KMP 不解决 UI 共享。Android 端你还是要用 Jetpack Compose 写一套 UI,iOS 端还是要用 SwiftUI 写一套 UI。
CMP 的出现,就是在 KMP 的基础上补上了"UI 共享"这块拼图。 它把 Jetpack Compose 的运行时和 Material3 组件库移植到了所有 KMP 支持的平台上:
· Android:直接使用系统内置的 Jetpack Compose,零额外开销
· iOS:通过 Skia 图形库原生渲染,触摸、滚动、文本输入等交互由 CMP 的 iOS 适配层处理
· Desktop:运行在 JVM 上,使用 Skiko(Skia 的 Kotlin 绑定)渲染,支持窗口管理和系统菜单
· Web:通过 Kotlin/Wasm 编译为 Wasm 二进制,在浏览器中运行,使用 Canvas 渲染
这意味着,你在 commonMain 中写的一个 @Composable 函数,在 Android 上是 Jetpack Compose 函数,在 iOS 上是原生 Skia 绘制,在桌面上是 JVM 渲染,在 Web 上是 Wasm 执行。一份代码,四端运行。
1.3 CMP vs 其他方案的深层对比
维度 Flutter React Native CMP
语言 Dart JavaScript/TypeScript Kotlin
UI 渲染 自绘引擎(Skia) 原生控件桥接 各平台原生渲染
与原生互操作 Platform Channel Native Module 直接调用
代码复用率 UI 100%,逻辑 100% UI 80%,逻辑 100% UI 90%+,逻辑 100%
学习曲线 中等(需学 Dart) 低(JS 生态) 低(Kotlin 开发者无缝迁移)
包体积 较大(含引擎) 中等 较小(Android 端几乎无额外开销)
生态成熟度 高 高 快速增长中
CMP 最大的差异化优势在于:对 Kotlin/Android 开发者来说,学习成本几乎为零。 你不需要学一门新语言,不需要理解新的构建系统,甚至不需要改变你写 Jetpack Compose 的习惯。你只是在 commonMain 中写代码,然后它自动在四个平台上跑起来。
1.4 版本与生态现状(截至 2026 年)
· CMP 1.11.x 是当前稳定版本
· iOS 最低支持:iOS 14.0(不再支持已废弃的 Apple x86_64 目标)
· 桌面端:内置 Compose Hot Reload,保存代码后 UI 即时更新
· Web 端:基于 Kotlin/Wasm,滚动性能显著改善,但暂不支持 Hot Reload
· Kotlin 版本:推荐 2.3.20 或更高
· JDK 要求:11 及以上
二、环境搭建
2.1 平台限制说明
CMP 本身是跨平台的,但 iOS 开发有一个硬性限制:编译 iOS 应用需要 macOS 主机,因为 Xcode 只能在 macOS 上运行。这是苹果生态的通用限制,并非 CMP 的缺陷。
你的操作系统 可开发目标
macOS Android + iOS + Desktop + Web
Windows Android + Desktop + Web
Linux Android + Desktop + Web
如果你使用 Windows 或 Linux,仍然可以完成本系列前 6 课的全部内容,第 7 课涉及 iOS 互操作时需要 macOS 配合。
2.2 安装 IDE
推荐使用 IntelliJ IDEA 或 Android Studio。两者共享相同的 Kotlin Multiplatform 核心功能,选择你更熟悉的即可。建议通过 JetBrains Toolbox App 安装,方便管理版本更新。
版本要求:
· IntelliJ IDEA 2025.2.2 或更高版本
· Android Studio Otter 2025.2.1 或更高版本
· 安装 Kotlin Multiplatform 插件(IDE 会自动提示安装)
2.3 配置 JDK
CMP 项目需要 JDK 11 或更高版本。推荐使用 JetBrains Runtime(JBR) ,因为 IntelliJ IDEA 发行版已内置 JBR,无需额外安装,且 JBR 对桌面端 KMP 应用有专门的兼容性优化。
验证 JDK 配置:在终端执行 java -version,确认输出版本号 ≥ 11。
2.4 配置 Android SDK
如果开发 Android 目标,需要确保系统环境变量 ANDROID_HOME 已正确设置。
macOS / Linux(Bash 或 Zsh) :在 ~/.profile 或 ~/.zprofile 中添加:
export ANDROID_HOME=~/Library/Android/sdk
Windows(PowerShell) :
[Environment]::SetEnvironmentVariable('ANDROID_HOME', '<SDK路径>', 'Machine')
Windows(CMD) :
setx ANDROID_HOME "<SDK路径>"
2.5 安装 Xcode(iOS 开发)
在 macOS 上,从 App Store 安装 Xcode。首次安装后务必手动启动一次 Xcode,让它完成初始组件下载和许可协议确认。每次 Xcode 更新后也需要重新启动以完成工具链更新。Kotlin Multiplatform 插件会自动进行预检,提示 Xcode 状态是否正常。
2.6 环境验证清单
完成上述安装后,逐项验证:
检查项 验证方式 预期结果
JDK java -version 版本 ≥ 11
IDE 启动 IDE,查看 Plugins Kotlin Multiplatform 插件已启用
Android SDK echo $ANDROID_HOME 显示有效路径
Xcode(macOS) xcode-select -p 显示 Xcode 路径
三、创建第一个项目
3.1 使用 IDE 向导创建
打开 IntelliJ IDEA(或 Android Studio),依次操作:
- 选择 File → New → Project
- 左侧面板选择 Kotlin Multiplatform
- 填写项目信息:
· Name:HelloCMP
· Group:com.example.hellocmp - 选择目标平台:勾选 Android、iOS、Desktop、Web(首次体验建议全选)
- 确保 Share UI 选项保持勾选状态——这是使用 Compose Multiplatform 共享 UI 的关键
- 点击 Create
IDE 会自动生成项目结构并下载依赖。首次创建可能需要几分钟。
3.2 理解项目结构
项目创建完成后,你会看到如下目录结构:
HelloCMP/
├── composeApp/ # 共享模块
│ └── src/
│ ├── commonMain/kotlin/ # ★ 共享代码所在的核心目录
│ │ └── com/example/hellocmp/
│ │ └── App.kt # 共享 UI 入口
│ ├── androidMain/kotlin/ # Android 平台特定代码
│ ├── iosMain/kotlin/ # iOS 平台特定代码
│ ├── desktopMain/kotlin/ # 桌面平台特定代码
│ └── wasmJsMain/kotlin/ # Web 平台特定代码
├── androidApp/ # Android 应用壳
├── desktopApp/ # 桌面应用壳
├── iosApp/ # iOS 应用壳(Xcode 项目)
├── gradle/
│ └── libs.versions.toml # 版本目录文件
├── build.gradle.kts # 根构建文件
└── settings.gradle.kts
关键理解:commonMain 是 CMP 的核心——你在这里编写 90% 以上的代码。各平台的 *Main 目录只包含平台特有的入口代码或适配层。判断标准很简单:如果一个功能在所有平台上行为一致,就放 commonMain;如果某个平台需要不同的实现,才在该平台的目录中写特定代码。
3.3 核心源码初读
打开 composeApp/src/commonMain/kotlin/…/App.kt,你会看到向导自动生成的代码:
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
Text("Hello, Compose Multiplatform!")
}
}
}
}
这段不到 20 行的代码,在四个平台上渲染出完全一致的 UI。关键元素:
· @Composable:标记这是一个 Compose 函数
· @Preview:允许在 IDE 中预览(Android Studio 支持,IntelliJ IDEA 需配合插件)
· MaterialTheme:应用 Material3 主题
· remember + mutableStateOf:状态管理
· Column:垂直布局容器
· Button、Text:Material3 组件
3.4 运行应用
Android:在运行配置下拉框中选择 composeApp,选择模拟器或真机,点击 Run。
桌面:选择 desktopApp 运行配置,点击 Run。桌面端会自动启用 Compose Hot Reload——修改代码后按 Ctrl+S/Cmd+S 保存,UI 即时更新而无需重启。
Web:选择 composeApp [wasmJs] 配置,运行后浏览器自动打开 http://localhost:8080/。
iOS:在 macOS 上,用 Xcode 打开 iosApp/ 目录,选择模拟器运行。或者从 IDE 的 iOS 运行配置启动。
四、代码深度解读
4.1 声明式 UI 范式:从"怎么做"到"是什么"
Compose 的核心理念是声明式 UI。理解这个范式转变,是掌握 Compose 的关键。
命令式 UI(传统 Android) :
TextView textView = findViewById(R.id.text);
textView.setText("Hello");
textView.setVisibility(View.VISIBLE);
你需要一步步告诉系统"怎么做"——找到视图、设置文本、设置可见性。
声明式 UI(Compose) :
if (showContent) {
Text("Hello")
}
你只需要描述"UI 应该是什么样子"——如果 showContent 为 true,就显示一段文本。至于如何更新屏幕、如何复用视图、如何避免闪烁,全部由 Compose 运行时处理。
这种范式转变带来的直接好处是:UI 代码与状态天然同步,不再需要手动同步视图和数据。
4.2 状态管理机制:Compose 的心脏
var showContent by remember { mutableStateOf(false) }
这行代码是 Compose 状态管理的核心,拆解来看:
mutableStateOf(false) 创建一个可观察的状态对象。Compose 运行时会追踪所有读取该状态的 @Composable 函数。当状态值改变时,这些函数会被标记为"需要重组"。
remember 确保状态在重组之间得以保留。Compose 的函数会被频繁重新执行(重组),如果没有 remember,每次重组都会重新创建状态对象,导致 UI 行为异常。remember 的作用是在首次组合时创建对象,后续重组时直接复用。
by 委托 让你可以直接用 showContent 读写值,等价于 state.value,但更简洁。
重组的触发与范围:当 showContent 的值改变时,Compose 只重新执行读取过该状态的 @Composable 函数,而不是重建整个 UI 树。这正是 Compose 高效的关键——局部刷新,而非整树重建。
4.3 布局系统:Modifier 链的威力
Column(
modifier = Modifier
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
)
Column 是垂直排列子元素的布局容器。Modifier 是 Compose 的修饰符链,safeContentPadding() 处理刘海屏和系统栏的安全区域,fillMaxSize() 让 Column 占满可用空间。
Modifier 链的顺序至关重要:
Modifier.padding(16.dp).background(Color.Red) // 先加内边距,再绘制背景(红色区域大)
Modifier.background(Color.Red).padding(16.dp) // 先绘制背景,再加内边距(红色区域小)
第一种写法中,背景覆盖了包含内边距的区域;第二种写法中,背景只覆盖内容区域,四周有 16dp 的透明边距。先 padding 后 background 让背景覆盖 padding 区域;先 background 后 padding 则背景不覆盖 padding。
4.4 各平台入口点:极简适配层
commonMain 中的 App() 函数不直接包含 main 函数。每个平台有自己的入口:
桌面端(desktopMain):
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "HelloCMP"
) {
App()
}
}
application 和 Window 来自 Compose Desktop,App() 是共享的 UI 入口。
Android(androidMain):
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
通过 setContent 将 Compose UI 挂载到 Activity。
iOS(iosMain):
fun MainViewController() = ComposeUIViewController { App() }
将 Compose UI 封装为 UIViewController,供 SwiftUI 或 UIKit 使用。
Web(wasmJsMain):通过 CanvasBasedWindow 将 Compose UI 渲染到 HTML Canvas 元素上。
这种设计的精髓在于:平台入口保持极简,全部业务和 UI 逻辑集中在共享代码中。 每个平台的入口代码通常不超过 20 行,你的核心工作永远在 commonMain。
五、常见陷阱与解决方案
在第一次创建和运行 CMP 项目时,以下问题最为常见:
陷阱 1:Gradle 同步失败,提示找不到 Kotlin Multiplatform 插件。 这通常是因为 IDE 版本过低或插件未安装。解决:升级 IDE 到要求版本,在 Plugins 中搜索并安装 Kotlin Multiplatform 插件。
陷阱 2:Android 运行配置找不到。 确保 ANDROID_HOME 环境变量已设置,且在 IDE 中配置了 Android SDK 路径(Settings → Languages & Frameworks → Android SDK)。
陷阱 3:桌面端运行时报错 “No JDK found”。 确保项目的 Gradle JDK 配置为 JBR 或 JDK 11+。在 Settings → Build, Execution, Deployment → Build Tools → Gradle 中检查 Gradle JDK 设置。
陷阱 4:iOS 编译失败,提示 Xcode 未配置。 在 macOS 上,确保已安装 Xcode 并至少启动过一次。运行 xcode-select -p 确认路径正确。
陷阱 5:Web 端运行后页面空白。 检查浏览器控制台是否有 Wasm 加载错误。确保使用支持 Wasm 的现代浏览器(Chrome 90+、Firefox 89+、Safari 15+)。
六、习题与参考答案
本课习题分为三类:概念理解(1-5 题)、代码实践(6-11 题)、综合设计(12-15 题)。建议全部动手完成,尤其是代码实践和综合设计题。
概念理解
习题 1:理解 Composable 函数
题目:以下代码有什么问题?
fun Greeting() {
Text("Hello")
}
参考答案:缺少 @Composable 注解。Text 是 Composable 函数,只能在其他 @Composable 函数中调用。正确写法:@Composable fun Greeting() { Text(“Hello”) }。
延伸理解:@Composable 注解会触发 Compose 编译器插件,在编译期对函数进行转换,注入重组所需的状态追踪逻辑。没有这个注解,编译器不会做任何特殊处理,自然无法调用其他 Composable 函数。
习题 2:remember 的作用
题目:如果去掉 remember,下面的计数器代码会怎样表现?
var count by mutableStateOf(0)
Button(onClick = { count++ }) { Text("Count: $count") }
参考答案:每次重组时 mutableStateOf(0) 都会被重新执行,count 被重置为 0。点击按钮触发重组后,计数显示始终为 0 或 1,无法累加。remember 的作用正是跨重组保留状态。
延伸理解:remember 的底层实现依赖于 Compose 的 Slot Table——一个在组合过程中维护的数据结构。首次组合时,remember 将值存入 Slot Table;后续重组时,直接从 Slot Table 中读取,实现状态保留。
习题 3:Modifier 链顺序
题目:解释 Modifier.padding(16.dp).background(Color.Red) 和 Modifier.background(Color.Red).padding(16.dp) 的区别。
参考答案:第一种写法先加 16dp 内边距,再在包含内边距的区域绘制红色背景,红色区域面积较大。第二种先绘制红色背景,再在红色区域内加内边距,红色区域等于内容区域,四周有 16dp 的透明边距。先 padding 后 background 让背景覆盖 padding 区域;先 background 后 padding 则背景不覆盖 padding。
习题 4:区分 Composable 和普通函数
题目:以下哪些函数可以在 @Composable 函数中调用?为什么?
A. Text(“Hello”)
B. println(“debug”)
C. Button(onClick = {}) { }
D. fun greet() = “Hello”
参考答案:A 和 C 可以调用,因为它们本身就是 @Composable 函数。B 可以调用,因为 println 是普通函数,Composable 函数内部可以调用任何非 Composable 函数(但不能反过来在普通函数中调用 Composable)。D 可以调用,因为调用普通函数不受限制。规则:@Composable 可以调用 @Composable 和普通函数;普通函数不能调用 @Composable。
习题 5:项目结构理解
题目:在项目中找到 androidMain、iosMain 和 desktopMain 目录。分别阅读它们中的入口文件。回答:如果要在所有平台上添加一个新的共享屏幕,代码应该写在哪个目录?
参考答案:在 composeApp/src/commonMain/kotlin/ 下创建新的 .kt 文件。所有平台共享的 Composable、状态逻辑、数据模型都写在这里。androidMain、iosMain、desktopMain 只包含平台入口和少量适配代码。判断标准:如果一个功能在所有平台上行为一致,就放 commonMain;如果某个平台需要不同的实现,才在该平台的目录中写特定代码。
代码实践
习题 6:创建新项目
题目:使用 IDE 向导创建一个名为 MyFirstApp 的 CMP 项目,只选择 Android 和 Desktop 两个目标平台,运行并确认在两个平台上都能正常工作。
参考答案:按 3.1 节步骤操作,在目标平台选择步骤中只勾选 Android 和 Desktop。项目创建后,在运行配置中先选择 desktopApp 运行,确认桌面窗口显示向导生成的 UI。再选择 composeApp 运行 Android 模拟器,确认 UI 一致。两个平台的 App() 函数来自同一份 commonMain 代码。
习题 7:添加文本和按钮
题目:修改 App.kt,实现以下 UI:顶部一个标题文本"我的第一个 CMP 应用",下方一个按钮"打招呼",点击后显示文本"你好,Compose Multiplatform!"。
参考答案:
@Composable
@Preview
fun App() {
MaterialTheme {
var greeting by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize().safeContentPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "我的第一个 CMP 应用",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(24.dp)
)
Button(onClick = { greeting = "你好,Compose Multiplatform!" }) {
Text("打招呼")
}
if (greeting.isNotEmpty()) {
Text(
text = greeting,
modifier = Modifier.padding(top = 16.dp)
)
}
}
}
}
习题 8:状态提升
题目:将习题 7 中的 greeting 状态从 App() 内部提升到一个新的 GreetingScreen(greeting: String, onGreet: () -> Unit) Composable 中。App() 只负责持有状态并传递。
参考答案:
@Composable
fun App() {
MaterialTheme {
var greeting by remember { mutableStateOf("") }
GreetingScreen(
greeting = greeting,
onGreet = { greeting = "你好,Compose Multiplatform!" }
)
}
}
@Composable
fun GreetingScreen(greeting: String, onGreet: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize().safeContentPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("我的第一个 CMP 应用", style = MaterialTheme.typography.headlineMedium)
Button(onClick = onGreet) { Text("打招呼") }
if (greeting.isNotEmpty()) {
Text(greeting, modifier = Modifier.padding(top = 16.dp))
}
}
}
这是状态提升模式:状态放在父组件中,子组件通过参数接收状态和回调。好处是 GreetingScreen 变成无状态组件,可复用性和可测试性更强。
习题 9:使用 Card 组件
题目:用 Card 包裹习题 7 中的标题文本,添加阴影效果和圆角。提示:使用 Card 的 elevation 参数和 Modifier 的 padding 参数。
参考答案:
Card(
modifier = Modifier.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Text(
text = "我的第一个 CMP 应用",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(24.dp)
)
}
Card 默认自带圆角(由 Material3 的 shape 决定),elevation 控制阴影深度。
习题 10:条件显示与 AnimatedVisibility
题目:使用 AnimatedVisibility 让习题 7 中的问候文本带淡入淡出动画。当 greeting 为空时隐藏,非空时渐现。
参考答案:
AnimatedVisibility(visible = greeting.isNotEmpty()) {
Text(
text = greeting,
modifier = Modifier.padding(top = 16.dp)
)
}
AnimatedVisibility 默认使用 fadeIn() + expandVertically() 作为进入动画,fadeOut() + shrinkVertically() 作为退出动画。也可手动指定 enter 和 exit 参数来自定义。
习题 11:TextField 输入
题目:在 App 中添加一个 TextField,让用户输入自己的名字。输入后,按钮点击时显示"你好,[名字]!"。
参考答案:
@Composable
fun App() {
MaterialTheme {
var name by remember { mutableStateOf("") }
var greeting by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize().safeContentPadding().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("请输入名字") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = { greeting = "你好,${name.ifBlank { "陌生人" }}!" },
modifier = Modifier.padding(top = 16.dp)
) {
Text("打招呼")
}
AnimatedVisibility(greeting.isNotEmpty()) {
Text(greeting, modifier = Modifier.padding(top = 16.dp))
}
}
}
}
综合设计
习题 12:平台特定代码
题目:在桌面端入口中修改窗口标题为"CMP 学习",并将窗口初始尺寸设置为 400×350 dp。思考:这些代码为什么不能放在 commonMain?
参考答案:
fun main() = application {
val state = rememberWindowState(
size = DpSize(400.dp, 350.dp),
position = WindowPosition(300.dp, 300.dp)
)
Window(
title = "CMP 学习",
onCloseRequest = ::exitApplication,
state = state
) {
App()
}
}
这些 API(rememberWindowState、DpSize、WindowPosition)只在 desktopMain 中可用,因为它们依赖 Compose Desktop 的窗口管理功能,而 Android、iOS、Web 没有"窗口"这个概念。这是平台特定代码的典型案例——只有当某个概念在特定平台上存在时,相关 API 才只能在该平台使用。
习题 13:Compose Hot Reload 体验
题目:运行桌面应用,然后修改 App() 中标题文本的颜色。观察 Hot Reload 是否自动更新 UI。记录修改前后从保存到界面变化的时间间隔。思考:Hot Reload 保留了哪些状态?
参考答案:在桌面上运行 desktopApp 配置。将标题的 style 改为 MaterialTheme.typography.headlineMedium.copy(color = Color.Blue),按 Ctrl+S/Cmd+S 保存。Compose Hot Reload 会在保存后自动重新编译并更新 UI,通常耗时 1 秒以内。状态(如用户输入的名字和已显示的问候文本)会被保留。
延伸思考:Hot Reload 保留的是 remember 持有的状态。如果修改涉及状态结构的变更(如新增或删除 remember 变量),部分状态可能会丢失。这是 Hot Reload 的固有限制,也是它与完整重启的区别。
习题 14:Web 平台运行
题目:将项目运行到 Web 平台,在浏览器中访问。然后在 App() 中添加一个 Text(“Web 平台也能运行!”),验证 Hot Reload 在 Web 上是否生效。思考:为什么 Web 端不支持 Hot Reload?
参考答案:选择 composeApp [wasmJs] 运行配置,浏览器打开后验证页面显示。修改代码后保存,Web 端目前不支持 Compose Hot Reload(截至 CMP 1.11.x),需要手动刷新浏览器页面。
延伸思考:Web 端 Hot Reload 的实现难度远高于桌面端。桌面端运行在 JVM 上,可以利用 JVM 的类重加载机制;而 Web 端编译为 Wasm 二进制,Wasm 模块一旦加载就无法动态替换。要实现 Hot Reload,需要更复杂的运行时架构支持,这是 CMP Web 端未来的改进方向之一。
习题 15:综合练习——待办清单
题目:在 commonMain 中实现一个简易待办清单:TextField 输入任务,按钮添加任务,LazyColumn 显示列表,每个列表项可点击删除。要求在 Android 和桌面端同时验证运行。思考:这个实现中,哪些部分在未来可以进一步优化?
参考答案:
@Composable
fun App() {
MaterialTheme {
var input by remember { mutableStateOf("") }
val tasks = remember { mutableStateListOf<String>() }
Column(
modifier = Modifier.fillMaxSize().safeContentPadding().padding(16.dp)
) {
Row(modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.weight(1f),
label = { Text("新任务") }
)
Button(
onClick = {
if (input.isNotBlank()) {
tasks.add(input)
input = ""
}
},
modifier = Modifier.padding(start = 8.dp)
) {
Text("添加")
}
}
LazyColumn(modifier = Modifier.padding(top = 16.dp)) {
items(tasks) { task ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable { tasks.remove(task) },
elevation = CardDefaults.cardElevation(2.dp)
) {
Text(
text = task,
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
}
mutableStateListOf 是 Compose 提供的可观察列表,添加或删除元素会自动触发重组。clickable Modifier 为卡片添加点击事件。items(tasks) 遍历列表生成子项。这段代码在 Android 和桌面端完全共享,无需任何平台特定代码。
延伸思考:这个实现有以下优化空间:
- 状态持久化:应用关闭后任务丢失,未来可引入 DataStore 或 SQLDelight 实现持久化(第5课内容)
- 状态管理架构:所有状态集中在 App() 中,未来可引入 ViewModel 分离逻辑(第6课内容)
- 导航:任务列表和编辑页面没有分离,未来可引入 Navigation Compose(第4课内容)
- 列表键值:items(tasks) 没有指定 key,未来可用 items(tasks, key = { it }) 优化重组性能
七、本课小结
本课完成了 Compose Multiplatform 从零到一的搭建和体验,核心要点如下:
技术定位:CMP 是 Kotlin Multiplatform 生态中共享 UI 层的解决方案,用一套 Kotlin 声明式代码覆盖 Android、iOS、桌面和 Web。它与 Flutter、React Native 的核心差异在于:对 Kotlin 开发者零学习成本,Android 端零额外开销,渐进式采用。
环境要点:JDK 11+、IDE 插件、Android SDK、Xcode(iOS 必需)。iOS 编译需要 macOS 是唯一的硬性平台限制。
项目结构:commonMain 是共享核心,各平台目录只做入口适配。判断代码该放哪里的标准:平台行为一致就放 commonMain,需要差异化实现才放平台目录。
代码范式:@Composable 函数 + remember / mutableStateOf 状态管理 + Modifier 修饰符链 = 声明式 UI 的三大支柱。状态变化自动触发重组,重组只重新执行读取过该状态的函数,实现局部刷新。
开发效率:桌面端 Hot Reload 将 UI 迭代周期从"编译-运行-查看"缩短到"保存-即时查看",极大提升开发体验。
下一步:你已经"把车发动起来"了。但要真正驾驭它,需要理解发动机的工作原理。
八、下一课预告
第2课 Compose 基础语法
更多推荐




所有评论(0)