之前研究那么久 KMP…但是直到现在也没写什么 demo 项目,这次因为身体原因刚好出不去,唉,正好有时间探索这个了。
[!NOTE]
目前只是个 demo ,之后我有时间有精力会慢慢添加功能
项目地址:https://github.com/grtsinry43/PureFlow
整个项目其实就写了四天,不过因为几乎 90% 的时间都在写代码,并且现在有 claude code 能干一点脏活累活,所以说自己写好重要部分就行。
它基于 Kotlin Multiplatform 和 Compose Multiplatform 技术栈,一套代码,同时支持 Android、iOS、macOS、Windows、Linux 甚至 Web 平台。
原本我计划用 ovCompose 跨到 HarmonyOS 的,但是没有 UI 库,先这样吧。
技术栈总览
Kotlin Multiplatform 和 Compose Multiplatform 无疑是项目首选,实现了几乎全栈 Kotlin,从 UI 到业务逻辑,甚至服务端和数据库交互,带来的是开发者开发效率提升和心智统一。
- 核心框架:
Kotlin Multiplatform: 跨平台的核心引擎。Compose Multiplatform: 声明式 UI 的未来,一套代码搞定多端 UI。
- 数据层:
SQLDelight: 类型安全的 SQL 数据库。Multiplatform Settings: 跨平台配置存储。
- 网络层:
Ktor Client: JetBrains 自家的 HTTP 客户端,跨平台支持很到位。RSS Parser: 使用com.prof18.rssparser开源解析库。Coil: 强大的图片加载和缓存库,多平台版本用起来也很香。
- 状态管理与异步:
StateFlow&SharedFlow: 响应式状态管理的核心,数据流转非常清晰。ViewModel: MVVM 架构模式中的核心组件。Kotlin Coroutines: 异步编程的利器,让复杂任务变得简单。
这几乎是一个纯正的 Kotlin 全家桶方案,也是我认为其值得一试的原因所在。
核心架构设计
PureFlow 设计成一个典型的模块化架构,就像搭积木一样,每块积木都有明确的功能和边界。
PureFlow/
├── composeApp/ # UI 层 - Compose Multiplatform 应用
├── shared/ # 共享业务逻辑层 (一次编写,逻辑共享)
├── server/ # 服务端 API (Ktor - 目前还未集成)
├── webApp/ # Web 前端 (React/TypeScript - 待完善或探索)
└── iosApp/ # iOS 特异性代码 (主要用于启动和平台特有集成)
1. shared 模块
包含了所有平台共享的业务逻辑、数据模型、数据库接口、网络请求封装和 RSS 解析器。它的 build.gradle.kts 配置编译到了各个平台目标:
// shared/build.gradle.kts
kotlin {
androidTarget {
compilations.all { kotlinOptions { jvmTarget = "11" } }
}
iosArm64() // iOS ARM64 设备
iosSimulatorArm64() // iOS 模拟器
jvm() // Desktop (Windows, macOS, Linux)
js {
moduleName = "shared"
browser() // 浏览器环境
binaries.library() // 打包成 JS 库
generateTypeScriptDefinitions() // Web 集成,自动生成 TS 定义
compilerOptions { target = "es2015" } // 兼容性考虑
}
}
通过这个配置,共享代码能编译成 Android 的 .jar、iOS 的 .framework、桌面端的 .jar 和 Web 用的 .js 库,真正实现了一套代码,多端运行。其中 generateTypeScriptDefinitions() 为 webApp 模块的 TypeScript 集成铺平了道路,TypeScript 可以直接使用 Kotlin 共享模块定义的接口和数据结构,减少了重复定义。
2. composeApp 模块:统一的 UI 界面
这里是 Compose Multiplatform 的战场…终于 iOS stable,可以放心去写。它负责所有平台的 UI 渲染。得益于 shared 模块提供的稳定业务逻辑,composeApp 可以专注于 UI 交互和视觉呈现。
// composeApp/build.gradle.kts 关键配置
kotlin {
androidTarget { /* ... */ }
listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true // 静态库便于集成
}
}
jvm() // Desktop 应用
}
isStatic = true 让 ComposeApp 模块在 iOS 上打包成静态框架,便于在 iosApp 中集成。
3. server 模块:轻量级后端支持(Planned)
虽然 PureFlow 主要是一个客户端应用,但我还是打算用 Ktor 实现一个轻量级后端。它的主要任务是提供 RSS 同步和用户数据管理功能,为将来的云同步、多设备数据一致性打下基础。
数据层:SQLDelight
数据存储是任何应用的核心,这里选择 SQLDelight 来处理本地数据库。它提供类型安全的 Kotlin API,避免了运行时错误,开发体验直线提升。
数据库设计
项目使用 SQLDelight 配置了数据库:
// shared/build.gradle.kts
sqldelight {
databases {
create("PureFlowDatabase") {
packageName.set("com.grtsinry43.pureflow.database")
}
}
}
实际的数据库表结构
从项目的 RssDatabase.sq 文件可以看到表设计:
RSS 条目表 (
rssItem)sqlCREATE TABLE rssItem ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, link TEXT NOT NULL, pubDate TEXT, pubDateTimestamp INTEGER, -- 添加时间戳字段用于排序 guid TEXT UNIQUE, -- 保证 GUID 唯一性 author TEXT, imageUrl TEXT, content TEXT, sourceUrl TEXT NOT NULL, -- RSS 源 URL createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) );订阅源表 (
subscription)sqlCREATE TABLE subscription ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL UNIQUE, -- RSS 源的唯一 URL name TEXT, -- 用户友好的订阅源名称 createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) );收藏表 (
favorite)sqlCREATE TABLE favorite ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, link TEXT NOT NULL, pubDate TEXT, pubDateTimestamp INTEGER, guid TEXT UNIQUE, -- 保证同一篇文章只能收藏一次 author TEXT, imageUrl TEXT, content TEXT, sourceUrl TEXT NOT NULL, -- 原始RSS源 sourceName TEXT, -- 收藏时记录的源名称,防止源被删除后无法显示 createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) );
性能优化索引设计:
-- rssItem 表索引
CREATE INDEX idx_rss_source_url ON rssItem(sourceUrl);
CREATE INDEX idx_rss_created_at ON rssItem(createdAt);
-- subscription 表索引
CREATE INDEX idx_subscription_url ON subscription(url);
-- favorite 表索引
CREATE INDEX idx_favorite_guid ON favorite(guid);
CREATE INDEX idx_favorite_created_at ON favorite(createdAt);
这些索引优化了常见查询的性能,比如按订阅源查询文章、按时间排序等操作。
expect/actual 机制在数据库驱动中的应用
还记得之前说的 expect/actual 吗?(现代 Android 开发探索)
它在这里完美解决了不同平台数据库驱动的差异:
// shared 模块中声明期望
expect object DatabaseModule {
fun provideDatabaseDriverFactory(): DatabaseDriverFactory
}
// 各平台模块中提供实际实现
// Android: AndroidSqliteDriver
// iOS: NativeSqliteDriver
// Desktop: SqliteDriver
这种分离让共享逻辑无需关心底层实现细节。
UI 实现细节:Compose Multiplatform 的响应式与主题
PureFlow 的 UI 完全由 Compose Multiplatform 构建,目标是实现真正的“一次编写,多端体验优化”。
实际的依赖配置
从 composeApp/build.gradle.kts 可以看到项目使用的 UI 相关依赖:
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(compose.materialIconsExtended)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.ksoup) // HTML解析
implementation(libs.coil.compose) // 图片加载
implementation(libs.coil.network.ktor)
implementation(projects.shared)
}
项目采用了完整的 Material 3 设计系统,使用 Coil 进行图片加载,KSoup 进行 HTML 解析。
主题系统
项目包含了完整的主题系统,在 THEME_GUIDE.md 中详细说明了如何使用和自定义主题。支持浅色/深色模式切换,以及 Material Design 3 的动态颜色特性。
(这部分是Claude Code写的)
网络层核心实现
网络请求选择了 Ktor Client,它的跨平台能力让不同平台复用了 HTTP 客户端的配置。
实际的 RSS 解析实现
项目使用了 com.prof18.rssparser 库而不是手动解析:
class FeedParser(
private val httpClient: HttpClient,
private val rssParser: RssParser
) {
suspend fun parseFeed(url: String): List<RssItem> {
return try {
// 1. 使用 Ktor 发起 GET 请求,获取 RSS 源的 XML 文本内容
val xmlContent = httpClient.get(url).bodyAsText()
// 2. 使用 RssParser 解析 XML 文本
// 这个库会自动识别是 RSS 还是 Atom 格式
val channel = rssParser.parse(xmlContent)
// 3. 将解析后的数据模型映射到我们自己的 RssItem
val rssItems = channel.items.map { feedItem ->
RssItem(
title = feedItem.title,
link = feedItem.link,
description = feedItem.description,
author = feedItem.author,
categories = feedItem.categories,
guid = feedItem.guid,
pubDate = feedItem.pubDate,
imageUrl = feedItem.image,
content = feedItem.content
)
}
rssItems
} catch (e: Exception) {
emptyList()
}
}
}
这种做法的好处是利用了成熟的开源库,避免了重复造轮子,同时保持了代码的简洁性。
平台特异性网络配置
shared/build.gradle.kts 为不同平台选择了合适的 HTTP 引擎:
// 平台特异性实现
androidMain.dependencies {
implementation(libs.ktor.client.android)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
jvmMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
这个没啥说的:Android 使用 Android 引擎,iOS 使用 Darwin 引擎,Desktop 使用 OkHttp 引擎。
数据管理:Repository 模式
项目采用了 Repository 模式来抽象数据源,实际的实现比较直接:
class RssRepository(
databaseDriverFactory: DatabaseDriverFactory,
private val feedParser: FeedParser
) {
private val database = PureFlowDatabase(databaseDriverFactory.createDriver())
private val queries = database.rssDatabaseQueries
suspend fun fetchAndCacheRssFeed(url: String): Result<List<RssItem>> {
return try {
// 1. 从网络获取RSS数据
val networkItems = feedParser.parseFeed(url)
// 2. 缓存到本地数据库(使用事务确保数据完整性)
database.transaction {
cacheRssItems(networkItems, url)
}
Result.success(networkItems)
} catch (e: Exception) {
Result.failure(e)
}
}
fun getRssItemsFromCache(url: String): Flow<List<RssItem>> {
return queries.selectBySourceUrl(url)
.asFlow()
.mapToList(Dispatchers.IO)
.map { dbItems ->
dbItems.map { dbItem ->
// 映射数据库对象到业务模型
RssItem(/* ... */)
}
}
}
}
这种实现直接使用 Kotlin 的 Result 类型处理成功/失败状态,并通过 SQLDelight 的 Flow 支持实现响应式数据更新。
服务层架构
项目实现了一个 RssServiceManager 作为核心服务管理器:
class RssServiceManager private constructor(
private val databaseDriverFactory: DatabaseDriverFactory,
private val feedParser: FeedParser
) {
companion object {
fun getInstance(
databaseDriverFactory: DatabaseDriverFactory,
feedParser: FeedParser
): RssServiceManager {
// 单例模式实现
}
}
val repository: RssRepository by lazy {
RssRepository(databaseDriverFactory, feedParser)
}
// 各种业务服务的懒加载实例
private val subscriptionService: SubscriptionService by lazy { /* ... */ }
private val incrementalUpdateService: IncrementalUpdateService by lazy { /* ... */ }
private val scheduledSyncService: ScheduledSyncService by lazy { /* ... */ }
}
这个服务管理器整合了所有核心功能,包括订阅管理、增量更新、定时同步等。
依赖注入:expect/actual 模式
项目采用了简洁的依赖注入方案,基于 expect/actual 模式:
expect object DatabaseModule {
fun provideDatabaseDriverFactory(): DatabaseDriverFactory
}
expect object NetworkModule {
fun provideHttpClient(): HttpClient
}
object ServiceModule {
private var serviceManager: RssServiceManager? = null
fun provideRssServiceManager(): RssServiceManager {
return serviceManager ?: run {
val databaseDriverFactory = DatabaseModule.provideDatabaseDriverFactory()
val httpClient = NetworkModule.provideHttpClient()
val rssParser = RssParser()
val feedParser = FeedParser(httpClient, rssParser)
val newServiceManager = RssServiceManager.getInstance(
databaseDriverFactory = databaseDriverFactory,
feedParser = feedParser
)
serviceManager = newServiceManager
newServiceManager
}
}
}
这种方式避免了重型 DI 框架的复杂性,同时保持了清晰的依赖关系。
构建和部署细节
项目实现了多平台的自动化构建和发布。
实际的打包脚本 (package.sh)
项目包含了一个真实的多平台打包脚本,支持:
if [[ "$OSTYPE" == "darwin"* ]]; then
AVAILABLE_FORMATS="dmg"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
AVAILABLE_FORMATS="deb rpm appimage"
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
AVAILABLE_FORMATS="msi exe"
fi
case $FORMAT in
"dmg")
./gradlew packageDmg
;;
"linux")
./gradlew packageDeb packageRpm packageAppImage
;;
"windows")
./gradlew packageMsi packageExe
;;
"all")
./gradlew packageDistributionForCurrentOS
;;
esac
桌面应用打包配置
从 composeApp/build.gradle.kts 可以看到详细的桌面应用打包配置,包括:
- 根据操作系统自动选择打包格式
- 平台特有的配置(如 macOS 的 Bundle ID,Windows 的升级 UUID)
- 应用程序的元数据和图标配置
CI/CD 流水线
GitHub Actions 配置实现了:
- 多平台构建矩阵: 在不同环境中分别构建各平台的安装包
- 标签驱动的自动发布: 基于 Git 标签自动触发发布流程
- 构建缓存优化: 使用 Gradle 缓存提升构建速度
- 详细的构建报告: 自动生成构建状态总结
总结与思考
这个项目算是一个活生生的案例,证明了在保证代码质量的同时,完全可以实现真正的一次编写、多端运行。
因为时间问题,这里选择了务实写法,使用成熟的第三方库而不是重复造轮子,采用简洁的依赖注入方案而不是重型框架
如果你也对跨平台开发感兴趣,或者正纠结于技术选型,希望这篇文章能为你带来一些启发和帮助。项目展现了现代 Kotlin 生态的强大能力,也让我对跨平台开发的未来充满信心。