Kotlin Academy · 课时

共享存储库层与用例层

将业务逻辑和数据访问移至 commonMain,以实现最大程度的复用。

第 3 / 4 课13 个步骤

共享存储库层与用例层 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. KMP 项目结构:commonMain、androidMain、iosMain
  2. 用于平台 API 的 expect/actual 机制
  3. 共享存储库层与用例层
  4. 使用 Koin 在 KMP 中进行依赖注入
← 返回 Kotlin Academy