为什么选择 Compose Multiplatform
传统跨平台方案(Flutter、React Native)都有各自的痛点:Flutter 使用 Dart 语言学习成本高,React Native 性能和原生体验有差距。Compose Multiplatform 基于 Kotlin 和 Jetpack Compose,让 Android 开发者可以用熟悉的技术栈实现真正的跨平台开发。
它支持的平台包括:
- Android
- iOS
- Desktop (Windows, macOS, Linux)
- Web (实验性)
项目搭建与配置
1. 创建项目
使用 Kotlin Multiplatform 插件创建项目,或使用 JetBrains 提供的模板:
kotlin
// build.gradle.kts (project level)
plugins {
kotlin("multiplatform") version "1.9.20" apply false
id("org.jetbrains.compose") version "1.5.10" apply false
}
2. 共享模块配置
kotlin
// shared/build.gradle.kts
kotlin {
androidTarget()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "shared"
isStatic = true
}
}
jvm("desktop")
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
}
}
val androidMain by getting {
dependencies {
implementation("androidx.activity:activity-compose:1.8.0")
}
}
val iosMain by creating {
dependsOn(commonMain)
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
}
}
}
}
共享 UI 代码实战
通用组件编写
在 commonMain 中编写跨平台 UI 组件:
kotlin
// commonMain/kotlin/ui/HomeScreen.kt
@Composable
fun HomeScreen() {
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "跨平台计数器",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "当前计数: $count",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { count-- }) {
Text("-")
}
Button(onClick = { count++ }) {
Text("+")
}
}
Spacer(modifier = Modifier.height(32.dp))
PlatformInfoCard()
}
}
@Composable
fun PlatformInfoCard() {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "平台信息",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = getPlatformName(),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
平台特定实现
expect/actual 机制
使用 Kotlin 的 expect/actual 机制实现平台特定逻辑:
kotlin
// commonMain/kotlin/Platform.kt
expect fun getPlatformName(): String
// androidMain/kotlin/Platform.android.kt
actual fun getPlatformName(): String {
return "Android ${android.os.Build.VERSION.SDK_INT}"
}
// iosMain/kotlin/Platform.ios.kt
import platform.UIKit.UIDevice
actual fun getPlatformName(): String {
return "iOS ${UIDevice.currentDevice.systemVersion}"
}
// desktopMain/kotlin/Platform.desktop.kt
actual fun getPlatformName(): String {
val os = System.getProperty("os.name")
val version = System.getProperty("os.version")
return "$os $version"
}
平台特定依赖
kotlin
// Android 特定:访问 Context
// androidMain
@Composable
actual fun getDeviceInfo(): String {
val context = LocalContext.current
return context.packageManager.getApplicationInfo(
context.packageName, 0
).loadLabel(context.packageManager).toString()
}
// iOS 特定:访问系统 API
// iosMain
import platform.Foundation.NSBundle
@Composable
actual fun getDeviceInfo(): String {
return NSBundle.mainBundle.bundleIdentifier ?: "Unknown"
}
资源管理
跨平台资源访问
kotlin
// 使用 Compose Resources
@Composable
fun AppLogo() {
Image(
painter = painterResource("ic_logo.xml"),
contentDescription = "App Logo",
modifier = Modifier.size(64.dp)
)
}
// 字符串资源
@Composable
fun WelcomeText() {
Text(text = stringResource("welcome_message"))
}
资源文件放在 commonMain/composeResources:
markdown
commonMain/
composeResources/
drawable/
ic_logo.xml
values/
strings.xml
导航与状态管理
使用 Voyager 实现跨平台导航
kotlin
// 添加依赖
commonMain {
dependencies {
implementation("cafe.adriel.voyager:voyager-navigator:1.0.0")
implementation("cafe.adriel.voyager:voyager-transitions:1.0.0")
}
}
// 定义 Screen
class HomeScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
Column {
Text("首页")
Button(onClick = { navigator.push(DetailScreen()) }) {
Text("查看详情")
}
}
}
}
class DetailScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
Column {
Text("详情页")
Button(onClick = { navigator.pop() }) {
Text("返回")
}
}
}
}
// App 入口
@Composable
fun App() {
MaterialTheme {
Navigator(HomeScreen())
}
}
共享 ViewModel
kotlin
// commonMain
class CounterViewModel {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value++
}
fun decrement() {
_count.value--
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = remember { CounterViewModel() }) {
val count by viewModel.count.collectAsState()
Column {
Text("计数: $count")
Button(onClick = { viewModel.increment() }) {
Text("+")
}
Button(onClick = { viewModel.decrement() }) {
Text("-")
}
}
}
网络请求与数据层
使用 Ktor 实现跨平台网络
kotlin
// 添加依赖
commonMain {
dependencies {
implementation("io.ktor:ktor-client-core:2.3.5")
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
}
}
androidMain {
dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.5")
}
}
iosMain {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.5")
}
}
// 网络客户端
class ApiClient {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
suspend fun fetchUsers(): List<User> {
return client.get("https://api.example.com/users")
.body()
}
}
@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)
平台入口配置
Android
kotlin
// androidApp/MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}
iOS
swift
// iosApp/ContentView.swift
struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea(.all)
}
}
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
kotlin
// iosMain/MainViewController.kt
fun MainViewController() = ComposeUIViewController {
App()
}
Desktop
kotlin
// desktopApp/Main.kt
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Compose Multiplatform App"
) {
App()
}
}
常见问题与解决方案
iOS 构建失败
确保 Xcode 和 CocoaPods 版本正确:
bash
# 安装 CocoaPods
sudo gem install cocoapods
# 清理构建
./gradlew cleanIosDebugSimulator
./gradlew cleanIosArm64
# 重新构建
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
资源文件找不到
确保资源目录结构正确,并在 build.gradle.kts 中配置:
kotlin
compose.resources {
publicResClass = true
packageOfResClass = "com.example.app.resources"
generateResClass = always
}
平台特定代码无法访问
使用 expect/actual 或依赖注入模式:
kotlin
// 定义接口
interface PlatformService {
fun doSomething(): String
}
// 在各平台实现
// androidMain
class AndroidPlatformService : PlatformService {
override fun doSomething() = "Android 实现"
}
// 通过依赖注入传递
@Composable
fun MyScreen(platformService: PlatformService) {
Text(platformService.doSomething())
}
性能优化建议
- 合理使用 remember:避免重复创建对象
kotlin
val viewModel = remember { MyViewModel() }
val list = remember { listOf(...) }
- 延迟加载:使用 LazyColumn/LazyRow
kotlin
LazyColumn {
items(largeList) { item ->
ItemRow(item)
}
}
- 图片优化:使用 Coil 或 Kamel 加载网络图片
kotlin
// 添加 Kamel 依赖
implementation("media.kamel:kamel-image:0.9.0")
// 使用
KamelImage(
resource = asyncPainterResource("https://..."),
contentDescription = null
)
- 减少重组:使用 derivedStateOf 和 key
kotlin
val filteredList by remember {
derivedStateOf { list.filter { it.active } }
}
实战项目示例
一个完整的待办事项应用:
kotlin
@Composable
fun TodoApp() {
var todos by remember { mutableStateOf(listOf<Todo>()) }
var inputText by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// 输入框
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("添加待办事项") }
)
Button(onClick = {
if (inputText.isNotBlank()) {
todos = todos + Todo(
id = todos.size,
text = inputText,
completed = false
)
inputText = ""
}
}) {
Text("添加")
}
}
Spacer(modifier = Modifier.height(16.dp))
// 待办列表
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(todos, key = { it.id }) { todo ->
TodoItem(
todo = todo,
onToggle = {
todos = todos.map {
if (it.id == todo.id) it.copy(completed = !it.completed)
else it
}
},
onDelete = {
todos = todos.filter { it.id != todo.id }
}
)
}
}
}
}
@Composable
fun TodoItem(
todo: Todo,
onToggle: () -> Unit,
onDelete: () -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Checkbox(
checked = todo.completed,
onCheckedChange = { onToggle() }
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = todo.text,
style = if (todo.completed) {
MaterialTheme.typography.bodyLarge.copy(
textDecoration = TextDecoration.LineThrough
)
} else {
MaterialTheme.typography.bodyLarge
}
)
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "删除")
}
}
}
}
data class Todo(
val id: Int,
val text: String,
val completed: Boolean
)
总结
Compose Multiplatform 为 Android 开发者提供了一条低学习成本的跨平台开发路径。通过共享 UI 代码和业务逻辑,可以大幅减少多平台维护成本。虽然目前 iOS 和 Web 支持还在完善中,但对于需要快速覆盖多端的项目来说,已经是一个可行的选择。
关键点回顾:
- 使用 expect/actual 处理平台差异
- 共享 UI 组件和业务逻辑
- 选择合适的跨平台库(Ktor、Voyager、Kamel)
- 注意性能优化和资源管理
- 合理规划平台特定代码的边界