docs(zh-CN): update

This commit is contained in:
neo
2026-03-13 17:45:44 +08:00
parent f548ca3e19
commit 4c0107a322
88 changed files with 16872 additions and 280 deletions

View File

@@ -0,0 +1,90 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin 编码风格
> 本文档在 [common/coding-style.md](../common/coding-style.md) 的基础上扩展了 Kotlin 相关内容。
## 格式化
* 使用 **ktlint****Detekt** 进行风格检查
* 遵循官方 Kotlin 代码风格 (`kotlin.code.style=official``gradle.properties` 中)
## 不可变性
* 优先使用 `val` 而非 `var` — 默认使用 `val`,仅在需要可变性时使用 `var`
* 对值类型使用 `data class`;在公共 API 中使用不可变集合 (`List`, `Map`, `Set`)
* 状态更新使用写时复制:`state.copy(field = newValue)`
## 命名
遵循 Kotlin 约定:
* 函数和属性使用 `camelCase`
* 类、接口、对象和类型别名使用 `PascalCase`
* 常量 (`const val``@JvmStatic`) 使用 `SCREAMING_SNAKE_CASE`
* 接口以行为而非 `I` 为前缀:使用 `Clickable` 而非 `IClickable`
## 空安全
* 绝不使用 `!!` — 优先使用 `?.`, `?:`, `requireNotNull()``checkNotNull()`
* 使用 `?.let {}` 进行作用域内的空安全操作
* 对于确实可能没有结果的函数,返回可为空的类型
```kotlin
// BAD
val name = user!!.name
// GOOD
val name = user?.name ?: "Unknown"
val name = requireNotNull(user) { "User must be set before accessing name" }.name
```
## 密封类型
使用密封类/接口来建模封闭的状态层次结构:
```kotlin
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String) : UiState<Nothing>
}
```
对密封类型始终使用详尽的 `when` — 不要使用 `else` 分支。
## 扩展函数
使用扩展函数实现工具操作,但要确保其可发现性:
* 放在以接收者类型命名的文件中 (`StringExt.kt`, `FlowExt.kt`)
* 限制作用域 — 不要向 `Any` 或过于泛化的类型添加扩展
## 作用域函数
使用合适的作用域函数:
* `let` — 空检查并转换:`user?.let { greet(it) }`
* `run` — 使用接收者计算结果:`service.run { fetch(config) }`
* `apply` — 配置对象:`builder.apply { timeout = 30 }`
* `also` — 副作用:`result.also { log(it) }`
* 避免深度嵌套作用域函数(最多 2 层)
## 错误处理
* 使用 `Result<T>` 或自定义密封类型
* 使用 `runCatching {}` 包装可能抛出异常的代码
* 绝不捕获 `CancellationException` — 始终重新抛出它
* 避免使用 `try-catch` 进行控制流
```kotlin
// BAD — using exceptions for control flow
val user = try { repository.getUser(id) } catch (e: NotFoundException) { null }
// GOOD — nullable return
val user: User? = repository.findUser(id)
```

View File

@@ -0,0 +1,18 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
- "**/build.gradle.kts"
---
# Kotlin Hooks
> 此文件在 [common/hooks.md](../common/hooks.md) 的基础上扩展了 Kotlin 相关内容。
## PostToolUse Hooks
`~/.claude/settings.json` 中配置:
* **ktfmt/ktlint**: 在编辑后自动格式化 `.kt``.kts` 文件
* **detekt**: 在编辑 Kotlin 文件后运行静态分析
* **./gradlew build**: 在更改后验证编译

View File

@@ -0,0 +1,147 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin 模式
> 此文件扩展了 [common/patterns.md](../common/patterns.md) 的内容,增加了 Kotlin 和 Android/KMP 特定的内容。
## 依赖注入
首选构造函数注入。使用 KoinKMP或 Hilt仅限 Android
```kotlin
// Koin — declare modules
val dataModule = module {
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
factory { GetItemsUseCase(get()) }
viewModelOf(::ItemListViewModel)
}
// Hilt — annotations
@HiltViewModel
class ItemListViewModel @Inject constructor(
private val getItems: GetItemsUseCase
) : ViewModel()
```
## ViewModel 模式
单一状态对象、事件接收器、单向数据流:
```kotlin
data class ScreenState(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false
)
class ScreenViewModel(private val useCase: GetItemsUseCase) : ViewModel() {
private val _state = MutableStateFlow(ScreenState())
val state = _state.asStateFlow()
fun onEvent(event: ScreenEvent) {
when (event) {
is ScreenEvent.Load -> load()
is ScreenEvent.Delete -> delete(event.id)
}
}
}
```
## 仓库模式
* `suspend` 函数返回 `Result<T>` 或自定义错误类型
* 对于响应式流使用 `Flow`
* 协调本地和远程数据源
```kotlin
interface ItemRepository {
suspend fun getById(id: String): Result<Item>
suspend fun getAll(): Result<List<Item>>
fun observeAll(): Flow<List<Item>>
}
```
## 用例模式
单一职责,`operator fun invoke`
```kotlin
class GetItemUseCase(private val repository: ItemRepository) {
suspend operator fun invoke(id: String): Result<Item> {
return repository.getById(id)
}
}
class GetItemsUseCase(private val repository: ItemRepository) {
suspend operator fun invoke(): Result<List<Item>> {
return repository.getAll()
}
}
```
## expect/actual (KMP)
用于平台特定的实现:
```kotlin
// commonMain
expect fun platformName(): String
expect class SecureStorage {
fun save(key: String, value: String)
fun get(key: String): String?
}
// androidMain
actual fun platformName(): String = "Android"
actual class SecureStorage {
actual fun save(key: String, value: String) { /* EncryptedSharedPreferences */ }
actual fun get(key: String): String? = null /* ... */
}
// iosMain
actual fun platformName(): String = "iOS"
actual class SecureStorage {
actual fun save(key: String, value: String) { /* Keychain */ }
actual fun get(key: String): String? = null /* ... */
}
```
## 协程模式
* 在 ViewModels 中使用 `viewModelScope`,对于结构化的子工作使用 `coroutineScope`
* 对于来自冷流的 StateFlow 使用 `stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initialValue)`
* 当子任务失败应独立处理时使用 `supervisorScope`
## 使用 DSL 的构建器模式
```kotlin
class HttpClientConfig {
var baseUrl: String = ""
var timeout: Long = 30_000
private val interceptors = mutableListOf<Interceptor>()
fun interceptor(block: () -> Interceptor) {
interceptors.add(block())
}
}
fun httpClient(block: HttpClientConfig.() -> Unit): HttpClient {
val config = HttpClientConfig().apply(block)
return HttpClient(config)
}
// Usage
val client = httpClient {
baseUrl = "https://api.example.com"
timeout = 15_000
interceptor { AuthInterceptor(tokenProvider) }
}
```
## 参考
有关详细的协程模式,请参阅技能:`kotlin-coroutines-flows`
有关模块和分层模式,请参阅技能:`android-clean-architecture`

View File

@@ -0,0 +1,83 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin 安全
> 本文档基于 [common/security.md](../common/security.md),补充了 Kotlin 和 Android/KMP 相关的内容。
## 密钥管理
* 切勿在源代码中硬编码 API 密钥、令牌或凭据
* 本地开发时,使用 `local.properties`(已通过 git 忽略)来管理密钥
* 发布版本中,使用由 CI 密钥生成的 `BuildConfig` 字段
* 运行时密钥存储使用 `EncryptedSharedPreferences`Android或 KeychainiOS
```kotlin
// BAD
val apiKey = "sk-abc123..."
// GOOD — from BuildConfig (generated at build time)
val apiKey = BuildConfig.API_KEY
// GOOD — from secure storage at runtime
val token = secureStorage.get("auth_token")
```
## 网络安全
* 仅使用 HTTPS —— 配置 `network_security_config.xml` 以阻止明文传输
* 使用 OkHttp 的 `CertificatePinner` 或 Ktor 的等效功能为敏感端点固定证书
* 为所有 HTTP 客户端设置超时 —— 切勿使用默认值(可能为无限长)
* 在使用所有服务器响应前,先进行验证和清理
```xml
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>
```
## 输入验证
* 在处理或将用户输入发送到 API 之前,验证所有用户输入
* 对 Room/SQLDelight 使用参数化查询 —— 切勿将用户输入拼接到 SQL 语句中
* 清理用户输入中的文件路径,以防止路径遍历攻击
```kotlin
// BAD — SQL injection
@Query("SELECT * FROM items WHERE name = '$input'")
// GOOD — parameterized
@Query("SELECT * FROM items WHERE name = :input")
fun findByName(input: String): List<ItemEntity>
```
## 数据保护
* 在 Android 上,使用 `EncryptedSharedPreferences` 存储敏感键值数据
* 使用 `@Serializable` 并明确指定字段名 —— 不要泄露内部属性名
* 敏感数据不再需要时,从内存中清除
* 对序列化类使用 `@Keep` 或 ProGuard 规则,以防止名称混淆
## 身份验证
* 将令牌存储在安全存储中,而非普通的 SharedPreferences
* 实现令牌刷新机制,并正确处理 401/403 状态码
* 退出登录时清除所有身份验证状态令牌、缓存的用户数据、Cookie
* 对敏感操作使用生物特征认证(`BiometricPrompt`
## ProGuard / R8
* 为所有序列化模型(`@Serializable`、Gson、Moshi保留规则
* 为基于反射的库Koin、Retrofit保留规则
* 测试发布版本 —— 混淆可能会静默地破坏序列化
## WebView 安全
* 除非明确需要,否则禁用 JavaScript`settings.javaScriptEnabled = false`
* 在 WebView 中加载 URL 前,先进行验证
* 切勿暴露访问敏感数据的 `@JavascriptInterface` 方法
* 使用 `WebViewClient.shouldOverrideUrlLoading()` 来控制导航

View File

@@ -0,0 +1,129 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin 测试
> 本文档扩展了 [common/testing.md](../common/testing.md),补充了 Kotlin 和 Android/KMP 特有的内容。
## 测试框架
* **kotlin.test** 用于跨平台 (KMP) — `@Test`, `assertEquals`, `assertTrue`
* **JUnit 4/5** 用于 Android 特定测试
* **Turbine** 用于测试 Flow 和 StateFlow
* **kotlinx-coroutines-test** 用于协程测试 (`runTest`, `TestDispatcher`)
## 使用 Turbine 测试 ViewModel
```kotlin
@Test
fun `loading state emitted then data`() = runTest {
val repo = FakeItemRepository()
repo.addItem(testItem)
val viewModel = ItemListViewModel(GetItemsUseCase(repo))
viewModel.state.test {
assertEquals(ItemListState(), awaitItem()) // initial state
viewModel.onEvent(ItemListEvent.Load)
assertTrue(awaitItem().isLoading) // loading
assertEquals(listOf(testItem), awaitItem().items) // loaded
}
}
```
## 使用伪造对象而非模拟对象
优先使用手写的伪造对象,而非模拟框架:
```kotlin
class FakeItemRepository : ItemRepository {
private val items = mutableListOf<Item>()
var fetchError: Throwable? = null
override suspend fun getAll(): Result<List<Item>> {
fetchError?.let { return Result.failure(it) }
return Result.success(items.toList())
}
override fun observeAll(): Flow<List<Item>> = flowOf(items.toList())
fun addItem(item: Item) { items.add(item) }
}
```
## 协程测试
```kotlin
@Test
fun `parallel operations complete`() = runTest {
val repo = FakeRepository()
val result = loadDashboard(repo)
advanceUntilIdle()
assertNotNull(result.items)
assertNotNull(result.stats)
}
```
使用 `runTest` — 它会自动推进虚拟时间并提供 `TestScope`
## Ktor MockEngine
```kotlin
val mockEngine = MockEngine { request ->
when (request.url.encodedPath) {
"/api/items" -> respond(
content = Json.encodeToString(testItems),
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
else -> respondError(HttpStatusCode.NotFound)
}
}
val client = HttpClient(mockEngine) {
install(ContentNegotiation) { json() }
}
```
## Room/SQLDelight 测试
* Room: 使用 `Room.inMemoryDatabaseBuilder()` 进行内存测试
* SQLDelight: 在 JVM 测试中使用 `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)`
```kotlin
@Test
fun `insert and query items`() = runTest {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
Database.Schema.create(driver)
val db = Database(driver)
db.itemQueries.insert("1", "Sample Item", "description")
val items = db.itemQueries.getAll().executeAsList()
assertEquals(1, items.size)
}
```
## 测试命名
使用反引号包裹的描述性名称:
```kotlin
@Test
fun `search with empty query returns all items`() = runTest { }
@Test
fun `delete item emits updated list without deleted item`() = runTest { }
```
## 测试组织
```
src/
├── commonTest/kotlin/ # Shared tests (ViewModel, UseCase, Repository)
├── androidUnitTest/kotlin/ # Android unit tests (JUnit)
├── androidInstrumentedTest/kotlin/ # Instrumented tests (Room, UI)
└── iosTest/kotlin/ # iOS-specific tests
```
最低测试覆盖率:每个功能都需要覆盖 ViewModel + UseCase。