用 CMP 构建跨平台博客应用:一次 Kotlin 的全栈实践
在追求高效开发的时代,跨平台技术已成为移动应用开发的主流选择。本文将介绍基于 CMP(Compose Multiplatform) 构建的开源博客应用 blog_kmp,展示如何用 Kotlin 实现跨平台的应用开发,共享代码并覆盖 Android、iOS 和 Desktop 平台。
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 平台上提供一致的界面体验,支持深色模式等特性。各平台界面经过优化,确保原生般的交互和视觉效果。
主要技术栈
- Ktor 客户端 - 用于网络请求,配置如下:
val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
suspend fun loadPosts(): List<Post> =
httpClient.get("https://cdn.julis/api/posts").body()
- DataStore - 提供跨平台数据库支持,实现数据缓存:
val dataKey = stringPreferencesKey(key)
val result = dataStore.data
.catch { exception ->
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)
}
}
- Koin - 用于依赖注入,简化模块管理:
val sharedModule = module {
single<PostRepository> { PostRepositoryImpl(get()) }
viewModel { PostViewModel(get()) }
}
- Kotlinx.Serialization - 处理 JSON 解析:
@Serializable
data class Post(
val id: String,
val title: String,
val content: String
)
- compose-webview-multiplatform - 集成 WebView 浏览器,支持跨平台内容展示:
val state = rememberWebViewState(postUrl)
WebView(state = state,modifier = Modifier.fillMaxSize())
平台特定实现
UI 层面三端使用同一份代码,但针对不同平台可能需要调整以实现最佳体验。例如,Android 端可使用 AndroidView 直接渲染原生 UI;iOS 端需要 XCode 配合处理开发者账号等配置;桌面端则利用 Compose Desktop 的窗口管理功能实现多开窗口。
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
DesktopAppTheme { AppContent() }
}
}
性能优化实践
为提高应用性能,实施了以下策略:分页加载通过 LazyColumn 实现懒加载,防止长列表卡顿;本地缓存结合 DataStore 离线存储和 Ktor 缓存策略,提升数据访问效率;图像处理使用 Coil 实现高效图片加载,优化内存和渲染性能。
LazyColumn {
itemsIndexed(posts) { _, post ->
PostItem(post)
}
item { if (loading) LoadingIndicator() }
}
HttpClient {
install(HttpCache)
}
AsyncImage(
modifier = Modifier.size(80.dp)
.shadow(
elevation = 5.dp,
shape = CircleShape,
spotColor = Color.Black
)
.clip(CircleShape)
.clickable { },
model = AppConfig.AVATAR,
contentDescription = AppConfig.AVATAR,
)
开发经验总结
使用 Compose 进行声明式 UI 开发,显著提高了布局效率。状态管理通过 mutableStateOf 和 derivedStateOf 实现响应式更新,导航通过 Compose Navigator 统一路由管理。异步编程使用 Kotlin Flow 简化网络请求,使代码更直观易维护。
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 } }
val gotoDebug: () -> Unit = {
navController.navigate(Routes.Debug())
}
val goToPostDetail: (Post) -> Unit = { it ->
navController.navigate(Routes.PostDetail(title = it.title, it.url))
}
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 等 Kotlin 技术在跨平台开发中的应用。KMP/CMP 技术能有效节省开发人力,实现多端代码共享,便于后续维护。但需要注意,相关库对 Kotlin/Java 版本要求较高,且对于原生依赖强的领域(如音视频)可能存在局限性,更适合偏交互业务的开发。项目源码已开源,可供参考。