用 CMP 构建跨平台博客应用:一次 Kotlin 的全栈实践

在追求高效开发的时代,跨平台技术已成为移动应用开发的主流选择,此前基于鸿蒙的开发平台开发 blog_harmony,将自己博客文章进行展示。本文将介绍基于 CMP(Compose Multiplatform) 构建的开源博客应用 blog_kmp,展示如何用 Kotlin 实现跨平台的应用开发。

Compose Multiplatform

Compose Multiplatform 是 JetBrains 推出的声明式 UI 框架,基于 Jetpack Compose 扩展而来:

  • 核心优势:用同一套 Kotlin 代码构建 Android、iOS、Desktop 和 Web 应用
  • 开发效率:实时预览、热重载加速开发迭代
  • 原生性能:通过 Skia 渲染引擎实现接近原生体验
  • 共享逻辑:业务逻辑、网络请求、状态管理可 100% 复用

项目架构与技术栈

blog_kmp 采用分层架构设计,核心模块包括:

shared/
├── src/commonMain/kotlin/ # 共享业务逻辑
│ ├── data/ # 数据层
│ ├── domain/ # 领域模型
│ └── presentation/ # UI状态管理
├── src/androidMain/ # Android 平台代码
└── src/iosMain/ # iOS 平台适配
├── composeApp
│   ├── build.gradle.kts
│   └── src
│   ├── androidMain # Android 平台代码
│   ├── commonMain # 共享业务逻辑
│ ├── App.kt # 界面展示入口
│ ├── data # 数据层
│ │ ├── api # 网络请求
│ │ ├── di # koin 依赖注入
│ │ ├── model # model 数据
│ │ └── repository # 数据缓存管理
│ │
│ ├── navigation # 页面间导航管理
│ ├── platform # 通过对各个平台抽象的接口
│ └── ui # 通用 UI 逻辑

│   ├── desktopMain # Desktop 平台适配
│   └── iosMain # iOS 平台适配

功能预览

Android

深色模式

iOS

Desktop

主要技术栈

  1. Ktor 客户端 - 网络请求
val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
suspend fun loadPosts(): List<Post> =
httpClient.get("https://cdn.julis/api/posts").body()
  1. DataStore - 跨平台数据库
val dataKey = stringPreferencesKey(key)
val result = dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val data: String? = preferences[dataKey]
if (data == null) {
null
} else {
if (isJson) Json.decodeFromString<T>(data) else (data as T)
}
}
  1. Koin - 依赖注入
val sharedModule = module {
single<PostRepository> { PostRepositoryImpl(get()) }
viewModel { PostViewModel(get()) }
}
  1. Kotlinx.Serialization - JSON 解析
@Serializable
data class Post(
val id: String,
val title: String,
val content: String
)
  1. compose-webview-multiplatform - WebView 浏览器
    使用的第三方开发compose-webview-multiplatform基于 java-cef开发,不过这个library 在 desktop 平台表现不是太好,待完善。
val state = rememberWebViewState(postUrl)
WebView(state = state,modifier = Modifier.fillMaxSize())

平台特定实现

UI 层面三端能够使用同一份代码,但为了体验,可能需要针对不同的设计,在桌面端可以设计更好地体验UI。这里避免不了 if-else 的UI逻辑,以及一些依赖各种系统的 api 需要单独实现,比如:深色模式监听、资源存储路径、系统信息、状态栏颜色等。

Android 端
Android 特定的功能结合使用起来非常的简单,毕竟都是有血缘关系的。可以使用 AndroidView 直接渲染原生的 UI 页面。

AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
MyView(context) }
},
update = { view ->}
)

iOS 端
iOS端主要需要 XCode 进行配合,还需要关注开发者账号相关的信息等,其他与 Android 端实现没有太大的差异。

桌面端
利用 Compose Desktop 的窗口管理,可以实现窗口多开。

fun main() = application {
Window(onCloseRequest = ::exitApplication) {
DesktopAppTheme { AppContent() }
}
}

🚀 性能优化实践

  1. 分页加载:实现懒加载防止长列表卡顿
LazyColumn {
itemsIndexed(posts) { _, post ->
PostItem(post)
}
item { if (loading) LoadingIndicator() }
}
  1. 本地缓存:DataStore 离线存储 + Ktor 缓存策略
HttpClient {
install(HttpCache) // 启用 HTTP 缓存
}
  1. 图像处理:搭配 Coil 实现高效图片加载
AsyncImage(
modifier = Modifier.size(80.dp)
.shadow(
elevation = 5.dp,
shape = CircleShape,
spotColor = Color.Black
)
.clip(CircleShape)
.clickable { },
model = AppConfig.AVATAR,
contentDescription = AppConfig.AVATAR,
)

开发经验总结

  1. UI界面
    使用 Compose 进行界面布局开发,声明性编程范式相比于传统的 xml 布局开发,高效很多,使用也很方便。使用了这种方式,传统的 UI 开发方式再也回不去了。

  2. 状态管理
    使用 mutableStateOf 实现响应式更新,或者使用 derivedStateOf 实现派生状态的处理。

var pagIndex by remember { mutableStateOf(0) }
var errorState by remember { mutableStateOf<String?>(null) }
val themeState by mineViewModel.appTheme.collectAsState()
val uiChecked by remember(themeState) { derivedStateOf { themeState == ThemeConstants.DARK } }
  1. 导航

实现 Compose Navigator 统一路由管理

val gotoDebug: () -> Unit = {
navController.navigate(Routes.Debug())
}

val goToPostDetail: (Post) -> Unit = { it ->
navController.navigate(Routes.PostDetail(title = it.title, it.url))
}

  1. Kotlin Flow
    简化异步编程,让网络请求的代码看起来更直观
fun loadAllPost(): Flow<List<PostV2>> = load("allPosts") {
postApi.getAllPost()?.data ?: emptyList()
}
suspend fun getAllPost(): SearchResponse? = request<SearchResponse>(getUrl("api/search.json"))

private suspend inline fun <reified T> request(url: String): T? {
return try {
client.get(url).body()
} catch (e: Exception) {
if (e is CancellationException) throw e
e.printStackTrace()
null
}
}

总结

经过一番各种折腾,将很多在工作上无法使用的能力(Koin、Flow、DataStore……)都体验使用了一下,在业余的时间完成了基于博客文章构建的 App 在三个平台上的开发,实际上最初我也想搭建 WebJs 的平台的,后面删除掉了,因为涉及到 web 平台开发的各种库相比客户端少很多,兼容起来也比较费劲。KMP/CMP 这块技术确实是能很大地节省开发人力,多端使用同一份UI逻辑代码,部分逻辑也可以用 kotlin 统一进行封装,后续维护也会方便很多。但这里有个缺点就是涉及到的库所需要的 kotlin/Java 版本要求比较高,除非开发一些独立的 App,否则公司里的项目想基于这些技术去实现不太大可能。以及如果所需要的能力比较依赖与原生,比如音视频领域就有一定的局限性,总体来讲更适合偏交互业务的开发。

项目源码: https://github.com/VomPom/blog_kmp