共享存储库层与用例层
将业务逻辑和数据访问移至 commonMain,以实现最大程度的复用。
共享存储库层与用例层 是 CoddyKit 上的免费 Kotlin Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Kotlin Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Kotlin Academy 课程共包含 4 节课。
架构目标
在 KMP 中,目标是尽可能将逻辑放入 commonMain:领域模型、仓库接口、用例和 ViewModel。只有平台 I/O 保留在平台源集中。
// Target architecture:
// commonMain:
// domain/ - data classes, interfaces
// data/ - repository implementations using shared clients
// usecase/ - business logic, composing repositories
// androidMain / iosMain:
// DI wiring, platform drivers (DB, network)commonMain 中的领域层
请在 commonMain 中定义领域实体和仓库接口。不要导入平台相关内容。
// commonMain/domain/User.kt
data class User(val id: String, val name: String, val email: String)
// commonMain/domain/UserRepository.kt
interface UserRepository {
suspend fun getUser(id: String): User?
suspend fun getAllUsers(): List<User>
suspend fun saveUser(user: User)
}使用 Ktor 的共享仓库实现
请使用 Ktor 客户端(支持多平台)在 commonMain 中实现仓库。实际的 HTTP 引擎由各个平台提供。
// commonMain/data/RemoteUserRepository.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
class RemoteUserRepository(private val client: HttpClient) : UserRepository {
override suspend fun getUser(id: String): User? =
client.get("https://api.example.com/users/$id").body()
override suspend fun getAllUsers(): List<User> =
client.get("https://api.example.com/users").body()
override suspend fun saveUser(user: User) {
client.post("https://api.example.com/users") { setBody(user) }
}
}用例层
用例(交互器)负责协调仓库并包含业务逻辑。它们是纯 Kotlin 代码,不依赖任何平台。
// commonMain/usecase/GetUserUseCase.kt
class GetUserUseCase(private val repo: UserRepository) {
suspend operator fun invoke(id: String): Result<User> =
runCatching { repo.getUser(id) ?: error("User not found: $id") }
}
// commonMain/usecase/SaveUserUseCase.kt
class SaveUserUseCase(private val repo: UserRepository) {
suspend operator fun invoke(user: User): Result<Unit> =
runCatching { repo.saveUser(user) }
}共享 ViewModel
ViewModel 可以使用 kotlinx-coroutines 和 StateFlow 放在 commonMain 中。Android 可以直接使用它,iOS 则通过 SwiftUI 对其进行包装。
// commonMain/viewmodel/UserViewModel.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class UserViewModel(private val getUser: GetUserUseCase) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val _user = MutableStateFlow<User?>(null)
val user = _user.asStateFlow()
fun load(id: String) {
scope.launch { _user.value = getUser(id).getOrNull() }
}
fun clear() = scope.cancel()
}Ktor HttpClient 配置
请在 commonMain 中创建 Ktor HttpClient,并使用共享配置。引擎(OkHttp / Darwin)由各个平台注入或创建。
// commonMain/network/HttpClientFactory.kt
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
fun createHttpClient(engine: io.ktor.client.engine.HttpClientEngine? = null) =
HttpClient(engine ?: io.ktor.client.engine.cio.CIO) {
install(ContentNegotiation) { json() }
}使用 SQLDelight 实现共享持久化
SQLDelight 会根据 commonMain 中的 .sq 文件,为所有平台生成类型安全的 Kotlin 查询。实际的数据库驱动程序由平台提供。
// commonMain/db/UserQueries.sq:
// CREATE TABLE User (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL);
// selectAll:
// SELECT * FROM User;
// Usage in commonMain:
// val db = UserDatabase(driver)
// val users = db.userQueries.selectAll().executeAsList()平台 DI 组装
请在平台特定的 DI 模块中将所有内容连接起来。接口是共享的,只有构造函数参数不同。
// androidMain:
val androidModule = module {
single { AndroidSqliteDriver(UserDatabase.Schema, androidContext(), "users.db") }
single { UserDatabase(get()) }
single<UserRepository> { RemoteUserRepository(get()) }
single { GetUserUseCase(get()) }
viewModel { UserViewModel(get()) }
}
// iosMain:
// Same pattern with NativeSqliteDriver在 commonTest 中测试用例
用例没有平台依赖,因此可以在 commonTest 中使用模拟仓库轻松地进行单元测试。
// commonTest/usecase/GetUserUseCaseTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
class FakeUserRepo : UserRepository {
override suspend fun getUser(id: String) = if (id == "1") User("1", "Alice", "a@b.com") else null
override suspend fun getAllUsers() = emptyList<User>()
override suspend fun saveUser(user: User) {}
}
class GetUserUseCaseTest {
private val useCase = GetUserUseCase(FakeUserRepo())
@Test
fun testGetExistingUser() = kotlinx.coroutines.runBlocking {
val result = useCase("1")
assertEquals("Alice", result.getOrNull()?.name)
}
}用例中的错误处理
请让用例返回 Result 或密封类,以便传递错误,而不会让异常跨越共享代码与平台代码之间的边界。
// commonMain:
sealed class UserResult {
data class Success(val user: User) : UserResult()
data class NotFound(val id: String) : UserResult()
data class Error(val cause: Throwable) : UserResult()
}
class SafeGetUser(private val repo: UserRepository) {
suspend operator fun invoke(id: String): UserResult = try {
val user = repo.getUser(id)
if (user != null) UserResult.Success(user) else UserResult.NotFound(id)
} catch (e: Exception) { UserResult.Error(e) }
}共享代码而不牺牲平台惯用方式
共享逻辑,而不是界面。Android 使用 Compose,iOS 使用 SwiftUI。两者都通过平台特定的绑定来观察同一个共享 ViewModel 状态。
// Android (Compose):
// val user by viewModel.user.collectAsState()
// iOS (SwiftUI):
// @State var user = viewModel.user.value
// viewModel.load(id: "1")
// Shared ViewModel:
// _user.value = ... (commonMain)
fun main() { println("Share logic; use platform UI frameworks") }快速检查
在结构良好的 KMP 项目中,哪一层不应包含平台特定的导入?
总结
请将领域模型、仓库接口、用例和共享 ViewModel 放入 commonMain。使用 Ktor(网络)和 SQLDelight(数据库),并在 DI 层注入平台引擎。在 commonTest 中使用模拟仓库测试业务逻辑。
用 AI 导师学习 Kotlin — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 51
- 课程
- 203
常见问题解答
「共享存储库层与用例层」课时是免费的吗?
是的 — 「共享存储库层与用例层」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Kotlin Academy 课程的其余内容,请升级到 CoddyKit PRO。 Kotlin Academy 课程共包含 4 节课。
「共享存储库层与用例层」这节课中我会学到什么?
将业务逻辑和数据访问移至 commonMain,以实现最大程度的复用。 你通过在浏览器中直接运行的动手代码来练习 Kotlin Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Kotlin Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Kotlin Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「共享存储库层与用例层」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Kotlin Academy 课中编写并运行代码吗?
能。每节 Kotlin Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。