Kotlin Compose Multiplatform 开发详解 – wiki基地


Kotlin Compose Multiplatform 开发详解

在跨平台应用开发领域,开发者们一直在寻求更高效、更便捷的解决方案。从早期的原生应用各自为战,到基于 Web 技术的跨平台框架(如 React Native、Ionic),再到利用特定渲染引擎的方案(如 Flutter),每一种技术都在努力平衡代码复用、性能和用户体验。

Kotlin Multiplatform (KMP) 的出现为这一领域带来了新的视角。KMP 允许开发者在不同平台(Android、iOS、JVM、Web、Native 等)之间共享业务逻辑代码,但 UI 层通常仍需要使用各平台的原生技术实现(Android 使用 View/Compose,iOS 使用 UIKit/SwiftUI)。虽然这解决了业务逻辑重复开发的痛点,但 UI 层的差异仍然是不少项目面临的挑战。

正是在这样的背景下,JetBrains 推出了 Compose Multiplatform。它将 Jetpack Compose 的声明式 UI 范式带到了 KMP 的世界,使得开发者可以在 Android 之外的平台(JVM Desktop、Web,以及未来可能的 iOS)上使用 Compose 构建 UI。这标志着跨平台开发迈向了新的阶段——共享 UI 代码成为可能。

本文将深入探讨 Kotlin Compose Multiplatform 的开发细节,涵盖其核心概念、项目结构、跨平台实现、以及在不同平台上的考量。

一、Compose Multiplatform 简介与核心优势

Compose Multiplatform (C/C/M) 是基于 Kotlin Multiplatform 技术栈的 UI 框架。它复用了 Jetpack Compose 的核心库和大部分 API,并在其基础上增加了对非 Android 平台的支持。简而言之,如果你熟悉 Android 上的 Jetpack Compose,那么学习 Compose Multiplatform 会非常自然。

其核心优势在于:

  1. 真正的 UI 代码共享 (部分平台):与 KMP 仅共享逻辑不同,C/C/M 允许你在 Android、JVM Desktop (Windows, macOS, Linux)、Web (通过 Canvas 或 Wasm) 等平台之间共享 UI 布局、组件和状态管理代码。这极大地减少了重复工作。
  2. 统一的技术栈:全程使用 Kotlin 语言开发,从业务逻辑到 UI,再到特定平台的集成,开发者可以使用一套语言和工具链。
  3. 声明式 UI:继承了 Compose 的声明式范式,UI 的构建更加直观、易于理解和维护。开发者只需描述 UI 的状态,框架会自动处理 UI 的更新。
  4. 优秀的用户体验:在 Android 和 Desktop 平台上,Compose 使用底层的图形库(如 Skia)进行高效渲染,接近原生性能。Web 端也在不断优化渲染方式。
  5. 强大的互操作性:可以轻松与现有平台的原生代码(Android Views、Swing/JavaFX、浏览器 DOM/JS)进行互操作。
  6. 活跃的社区与生态:得益于 Kotlin 和 Jetpack Compose 庞大的开发者基础,Compose Multiplatform 正在快速发展,生态系统也在逐步完善。

需要明确的是关于 iOS 的支持:

  • 当前成熟度:Compose Multiplatform 目前主要面向 Android、JVM Desktop 和 Web。
  • iOS UI 的现状:Compose Multiplatform 不直接 生成原生的 iOS UI 组件 (UIKit/SwiftUI)。它提供的是一种实验性的、基于 Skia 渲染的 UI 路径,或者最常见的 KMP 用法是共享业务逻辑,而 UI 仍然在 iOS 端使用 Swift/SwiftUI 或 Objective-C/UIKit 独立实现。因此,如果你期望用一套 Compose 代码同时运行在 Android 和 原生 iOS UI 上,目前 Compose Multiplatform 的主要用例 不是 这样。它更侧重于 Android/Desktop/Web 的 UI 共享,以及 iOS 的逻辑共享。理解这一点非常重要,避免误解。未来的发展可能会改变这一点,但这是当前的实际情况。

本文的重点将主要放在 Compose Multiplatform 在 Android、Desktop 和 Web (Wasm/JS) 上的 UI 共享实现。

二、Compose Multiplatform 的核心概念

Compose Multiplatform 复用了 Jetpack Compose 的核心 API,因此以下概念对于理解 C/C/M 至关重要:

  1. 声明式 UI (Declarative UI)
    与传统的命令式 UI (如 Android Views 的 XML 布局 + Java/Kotlin 代码控制) 不同,声明式 UI 只需描述 UI 在给定状态下应该是什么样子。当状态变化时,框架会自动重新执行相关的 UI 代码并更新屏幕。
    kotlin
    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!")
    }

    这个函数描述了当输入一个 name 时,应该显示一个包含 “Hello [name]!” 的文本。

  2. Composable 函数 (@Composable)
    这是 Compose 的基本构建单元。所有构建 UI 的函数都需要使用 @Composable 注解标记。Composable 函数可以相互调用,形成 UI 树。它们没有返回值(或者说返回 Unit),其作用是通过调用其他 Composable 函数来描述 UI。

  3. 状态 (State) 与可变状态 (MutableState)
    UI 是随数据变化的。Compose 使用“状态”来跟踪可能随时间变化的数据。当状态变化时,Compose 会自动“重组”(Recompose)相关的 UI 部分以反映新的状态。
    MutableState<T> 是 Compose 中最常见的可变状态持有者。
    “`kotlin
    import androidx.compose.runtime.*

    @Composable
    fun Counter() {
    var count by remember { mutableStateOf(0) } // 使用 remember 保存状态并在重组时记住它

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
    

    }
    ``remember帮助 Composable 在重组时保留状态,mutableStateOf` 创建一个可被观察的可变状态。

  4. 重组 (Recomposition)
    当 Composable 函数依赖的状态发生变化时,Compose 会重新执行该函数及其相关的子函数,这个过程称为重组。Compose 足够智能,只会重组那些受到状态变化影响的部分,而不是整个 UI 树,从而保证效率。

  5. 修饰符 (Modifiers)
    修饰符是用于装饰或增强 Composable 的元素。它们可以改变 Composable 的外观、布局行为、添加交互等。修饰符通过链式调用的方式应用。
    kotlin
    Text(
    text = "Hello",
    modifier = Modifier
    .padding(16.dp) // 添加内边距
    .background(Color.Blue) // 设置背景颜色
    .clickable { /* handle click */ } // 使其可点击
    )

    Modifier 是 Compose Multiplatform 中跨平台共享 UI 样式的关键。

  6. 布局 (Layout)
    Compose 提供了灵活的布局机制。常见的布局 Composable 包括:

    • Column: 垂直排列子元素。
    • Row: 水平排列子元素。
    • Box: 将元素叠放在一起(类似 FrameLayout 或 Div)。
      通过组合这些基础布局和使用修饰符(如 width, height, padding, align, weight 等),可以构建复杂的布局结构。Compose 的布局过程是基于约束的,高效且强大。
  7. Composition (组合)
    Compose UI 是一个 Composable 函数的树形结构,这棵树被称为 Composition。Compose 框架管理着 Composition 的生命周期,包括初次创建和后续的重组。

  8. 副作用 (Side-Effects)
    在 Composable 函数内部执行修改应用状态(如更新数据库、发送网络请求、启动协程)的操作被称为副作用。Compose 提供了一系列 API 来安全地管理副作用,确保它们在正确的时机执行,并且在重组或 Composable 离开 Composition 时能够被取消或清理,例如 LaunchedEffect, SideEffect, DisposableEffect, rememberCoroutineScope 等。

三、项目结构与跨平台实现

一个典型的 Compose Multiplatform 项目基于 Gradle 构建系统,并遵循 Kotlin Multiplatform 的模块结构。项目通常包含以下模块:

  1. commonMain: 这是共享模块的核心。所有跨平台的业务逻辑、数据模型、接口定义以及 跨平台的 Composable UI 代码 都放在这里。commonMain 不依赖于任何特定平台。
  2. androidMain: 包含 Android 平台的特定代码。在这里,你会启动 Compose UI,集成 Android 平台的 API,或者实现 commonMain 中定义的 expect 接口的 Android actual 实现。Compose UI 在 Android 上通过 setContent 函数嵌入到 Activity 或 Fragment 中。
  3. desktopMain: 包含 JVM Desktop 平台的特定代码。在这里,你会创建和配置桌面窗口,启动 Compose UI。desktopMain 通常会调用 commonMain 中的根 Composable 函数来构建 UI。
  4. jsMain: 包含 Web 平台的特定代码。目前 Compose for Web 主要通过 WebAssembly 或 JavaScript/Canvas 渲染。在这里,你会集成 Web API,启动 Compose UI 并将其挂载到 HTML 元素上。最新的发展倾向于使用 WebAssembly。
  5. iosMain: 包含 iOS 平台的特定代码。如前所述,目前 Compose Multiplatform 的主要价值在于共享逻辑。iosMain 会实现 commonMain 中定义的 expect 接口的 iOS actual 实现,供 Swift/Objective-C 代码调用。注意: 如果尝试使用实验性的 Skia 渲染 UI,相关的启动代码也会在这里,但这与原生 iOS UI 集成是不同的路径。

跨平台 UI 实现的关键:

  • 共享 Composable 函数:将大部分 UI 组件、屏幕布局、导航结构(如果使用 multiplatform navigation library)定义在 commonMain 中。这些 Composable 函数使用 Compose 的核心 API 和修饰符,它们本身是跨平台的。
  • 平台特定入口点:每个平台模块 (androidMain, desktopMain, jsMain) 会有一个主入口点,在这个入口点中调用 commonMain 中定义的根 Composable 函数。
    • Android: 在 Activity 的 onCreate 中调用 setContent { App() },其中 App()commonMain 中的根 Composable。
    • Desktop: 在 main 函数中创建窗口并调用 application { Window { App() } }
    • Web (Wasm/JS): 在 JavaScript 入口文件中调用 compose.web.renderComposable(id = "root") { App() }
  • 处理平台差异:虽然 UI 大部分共享,但有时需要根据平台调整行为或外观,或者访问平台特有的功能(如文件系统、传感器、原生对话框等)。
    • 逻辑差异: 使用 KMP 的 expect/actual 机制在 commonMain 中定义接口,然后在各个平台模块中提供具体的实现。共享 UI 代码通过调用 commonMain 中的 expect 函数来间接调用平台特定功能。
    • UI 差异: 有时需要在 Composable 函数内部根据当前运行的平台显示不同的内容或应用不同的修饰符。可以通过检查平台信息(如果共享模块能获取到的话,或者通过依赖注入传递)或者使用特定的库来实现。更常见的是,一些低级别的平台集成(如文件选择器、通知)还是需要在平台层实现并通过 expect/actual 暴露给共享 UI。

四、深入实现:构建一个简单的跨平台 UI

让我们构思一个简单的跨平台应用:一个计数器,带有一个按钮点击增加计数,并在文本框中显示当前计数。

1. commonMain 模块

commonMain 中创建核心 UI Composable:

“`kotlin
// commonMain/kotlin/App.kt
package com.yourcompany.counterapp

import androidx.compose.foundation.layout.
import androidx.compose.material.

import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun App() {
// 跨平台共享的状态
var count by remember { mutableStateOf(0) }

MaterialTheme { // 使用 Material Design 主题
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Count: $count",
            fontSize = 24.sp,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        Button(onClick = { count++ }) {
            Text("Increase Count")
        }
    }
}

}
``
这个
App()Composable 使用了remember,mutableStateOf,Column,Text,Button,Modifier,padding,fillMaxSize,Arrangement.Center,Alignment.CenterHorizontally,MaterialTheme` 等 Compose API。这些 API 在所有支持的 Compose Multiplatform 目标上都是可用的。

2. androidMain 模块

androidMain 中启动这个 App Composable:

“`kotlin
// androidMain/kotlin/com/yourcompany/counterapp/MainActivity.kt
package com.yourcompany.counterapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App() // 调用 commonMain 中的 App Composable
}
}
}
``
这是一个标准的 Android Activity,它使用
setContent` 函数将 Compose UI 设置为 Activity 的内容。

3. desktopMain 模块

desktopMain 中创建窗口并启动 App Composable:

“`kotlin
// desktopMain/kotlin/main.kt
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.yourcompany.counterapp.App // 导入 commonMain 中的 App

fun main() = application {
Window(onCloseRequest = ::exitApplication, title = “Counter App”) {
App() // 调用 commonMain 中的 App Composable
}
}
``applicationWindow` 是 Compose for Desktop 提供的 API,用于创建桌面应用窗口。

4. jsMain (Web) 模块

对于 Web 平台,通常会有一个 index.html 文件和一个 Kotlin/JS 入口文件。

“`html





Counter App (Web)





“`

“`kotlin
// jsMain/kotlin/main.kt
import com.yourcompany.counterapp.App // 导入 commonMain 中的 App
import org.jetbrains.skiko.wasm.onWasmReady // or org.jetbrains.skiko.Clocks, depends on setup
import org.jetbrains.compose.web.renderComposable

fun main() {
// For Wasm:
onWasmReady { // or similar entry point depending on template
renderComposable(rootElementId = “root”) { // Use renderComposable for Web
App() // 调用 commonMain 中的 App Composable
}
}
// For legacy Canvas/JS:
/
window.onload = {
org.jetbrains.compose.web.renderComposable(rootElementId = “root”) {
App()
}
}
/
}
``
这里的
renderComposable函数将App` Composable 渲染到 HTML 中 id 为 “root” 的元素内。渲染机制(Canvas 或 DOM/Wasm)取决于你的构建配置和 Compose for Web 的版本。最新的趋势是基于 Wasm 和 DOM 元素。

通过这种方式,同一个 App() Composable 代码库就能够在 Android、Desktop 和 Web (Wasm/JS) 上运行,实现了 UI 层的代码共享。

五、平台特定集成与注意事项

虽然大部分 UI 代码可以共享,但在实际应用中,你总会遇到需要与平台特定 API 集成的情况,或者需要根据平台调整 UI 的细微之处。

  1. 使用 expect/actual 访问平台服务
    如前所述,对于文件操作、网络状态、传感器、原生通知、权限请求等平台独有的功能,应使用 expect/actual 机制。

    • commonMain 中定义一个 expect classexpect fun
    • 在各个平台模块中提供 actual 实现。
    • 共享 Composable 通过调用 commonMain 中的 expect 声明来间接使用平台功能。
  2. 处理平台外观差异
    Compose Multiplatform 提供了 Material Design 组件库,这些组件在不同平台上的外观可能略有差异,以尽可能符合平台惯例。例如,Button 的形状、字体渲染、滚动行为等。大多数情况下,这种差异是可接受甚至是期望的,因为它让应用看起来更“原生”。如果需要更精细的控制,可以:

    • 使用较低级别的 Compose Foundation API 构建自定义组件。
    • commonMain 中通过参数或接口,允许平台特定代码注入不同的 UI 片段或修饰符。
  3. 导航
    Compose Multiplatform 没有官方的跨平台导航库。目前主要有几种选择:

    • 使用为 Compose Multiplatform 设计的第三方库(如 voyager)。
    • commonMain 中定义导航逻辑(例如,使用 sealed class 表示屏幕状态),然后每个平台在自己的入口点根据状态切换 Composable。这需要一些手动实现。
    • 在 Android 上使用 Jetpack Navigation Compose,但在 Desktop 和 Web 上需要实现一套独立的导航逻辑,或者找到兼容方案。
      导航是 Compose Multiplatform 领域一个仍在发展的区域。
  4. 资源管理
    字体、图片、字符串等资源的管理在跨平台项目中需要统一。Compose Multiplatform 支持跨平台的资源管理。通常在 commonMain 中定义资源,并通过特定的方式在各平台引用。可以使用 expect/actual 来加载平台特有的资源,或者使用第三方库提供的跨平台资源方案。

  5. 性能优化
    Compose 的性能优化很大程度上依赖于编写高效的 Composable 函数和正确管理状态,避免不必要的重组。跨平台开发额外需要考虑的是不同平台的渲染特性和性能瓶颈。

    • Android/Desktop: 主要依赖 Skia 渲染,性能通常很高。
    • Web (Canvas): 性能取决于浏览器和设备,复杂 UI 可能遇到瓶颈。
    • Web (Wasm/DOM): 利用浏览器原生能力,性能潜力大,但 Wasm 本身有启动开销。
      性能调优需要针对特定平台进行。使用 Compose 提供的性能分析工具(如 Layout Inspector, Compose Tracing)是必要的。
  6. 测试
    Compose Multiplatform 项目的测试可以在多个层面进行:

    • 单元测试: 在 commonTest 中对共享的业务逻辑、数据模型进行单元测试。
    • UI 测试: Compose 提供了 UI 测试 API。可以在 Android 端运行基于 JVM 的 Compose UI 测试,也可以在 Desktop 和 Web (实验性) 上运行。这些测试可以验证 Composable 的布局和行为。

六、挑战与未来发展

虽然 Compose Multiplatform 潜力巨大,但它仍然面临一些挑战:

  • 成熟度:相对于原生开发或更成熟的跨平台框架,Compose Multiplatform,特别是 Web 和 iOS UI 部分,仍在快速发展阶段,API 可能发生变化,生态系统仍在建设中。
  • Web 支持:Compose for Web 的渲染机制和性能仍在迭代中,与复杂的 Web 前端框架(如 React, Vue)相比,功能和生态仍有差距。Wasm 是一个重要的发展方向。
  • iOS UI 集成:如前所述,缺乏成熟的原生 iOS UI 集成是其当前的主要限制,但这可能是未来发展的一个重点方向。
  • 社区与生态:虽然增长迅速,但特定于 Compose Multiplatform 的库、工具和社区资源仍不如 Android Compose 或其他成熟平台丰富。

未来发展趋势:

  • WebAssembly (Wasm):Wasm 作为 Compose for Web 的主要目标,将带来更好的性能和与 Web 生态的集成。
  • 工具链增强:JetBrains 会持续改进 IDE 支持、调试工具、构建流程等。
  • 生态系统扩张:更多第三方库将支持 Compose Multiplatform,涵盖导航、状态管理、网络、数据库等方面。
  • 潜在的 iOS UI 发展:尽管面临挑战,但社区和 JetBrains 可能会在 Skia 渲染之外探索更多可能性,或者优化当前的 Skia 路径,使其在 iOS 上更实用。

七、总结

Kotlin Compose Multiplatform 是跨平台开发领域一个令人兴奋的新选择。它通过将 Jetpack Compose 的声明式 UI 能力扩展到 Android 之外的平台,实现了 UI 和业务逻辑代码的高度共享。对于希望利用 Kotlin 统一技术栈并在 Android、Desktop 和 Web 上构建应用的团队来说,Compose Multiplatform 提供了一条高效的路径。

尽管它在 Web 和 iOS UI 支持方面仍处于发展阶段,面临一些挑战,但其核心优势——高效的 UI 代码共享、统一的 Kotlin 语言、优秀的性能和良好的开发者体验——使其成为未来跨平台开发的重要方向。随着技术的不断成熟和生态的日益完善,Compose Multiplatform 无疑将在构建现代跨平台应用中扮演越来越重要的角色。

如果你是 Android Compose 开发者,或者对使用 Kotlin 进行跨平台开发感兴趣,现在正是深入了解和尝试 Kotlin Compose Multiplatform 的好时机。开始一个示例项目,探索其可能性,并加入这个快速发展的社区吧!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部