android方案 : Multiplatform 跨平台开发实战:一套代码,多端运行

为什么选择 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())
}

性能优化建议

  1. 合理使用 remember:避免重复创建对象
kotlin 复制代码
val viewModel = remember { MyViewModel() }
val list = remember { listOf(...) }
  1. 延迟加载:使用 LazyColumn/LazyRow
kotlin 复制代码
LazyColumn {
    items(largeList) { item ->
        ItemRow(item)
    }
}
  1. 图片优化:使用 Coil 或 Kamel 加载网络图片
kotlin 复制代码
// 添加 Kamel 依赖
implementation("media.kamel:kamel-image:0.9.0")

// 使用
KamelImage(
    resource = asyncPainterResource("https://..."),
    contentDescription = null
)
  1. 减少重组:使用 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)
  • 注意性能优化和资源管理
  • 合理规划平台特定代码的边界
相关推荐
百度一下吧1 小时前
CSS3 之后新增的特性:从 CSS4 到现代 CSS 的演进
前端·css·css3
爱喝热水的呀哈喽1 小时前
hm探查。
java·前端·人工智能
2603_965896621 小时前
JS ES6常用新特性|let/const、解构赋值、展开运算符
前端·javascript·es6
衡石科技1 小时前
一个门户只保留一个智能助理:衡石嵌入式 Data Agent 的运行与渲染边界
前端·javascript·html·data agent
Hilaku1 小时前
一个前端工程师除了写代码,还能靠什么赚钱?
前端·javascript·程序员
程序员Sunday1 小时前
JavaScript 闭包是什么?为什么变量没有被释放,什么时候会内存泄漏?
前端·javascript
JarvanMo1 小时前
Flutter 终于有自己的 React Flow 了,vyuh_node_flow 上手实录
前端