用 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()
  2. 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)
    }
    }
  3. Koin - 依赖注入

    val sharedModule = module {
    single<PostRepository> { PostRepositoryImpl(get()) }
    viewModel { PostViewModel(get()) }
    }
  4. Kotlinx.Serialization - JSON 解析

    @Serializable
    data class Post(
    val id: String,
    val title: String,
    val content: String
    )
  5. 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() }
    }
  2. 本地缓存:DataStore 离线存储 + Ktor 缓存策略

    HttpClient {
    install(HttpCache) // 启用 HTTP 缓存
    }
  3. 图像处理:搭配 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,
    )

    开发经验总结

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

  5. 状态管理
    使用 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