@pilllesss/yorn 1.0.182 → 1.0.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,1018 @@
1
+ # Kotlin / Android Code Review Guide
2
+
3
+ > Kotlin/Android 代码审查指南,覆盖协程作用域与取消、Flow 陷阱、Compose 重组、空安全、内存泄漏、架构分层与密封类状态建模等核心主题。
4
+
5
+ ## 目录
6
+
7
+ - [协程:作用域与取消](#协程作用域与取消)
8
+ - [Flow 陷阱](#flow-陷阱)
9
+ - [Jetpack Compose 重组](#jetpack-compose-重组)
10
+ - [空安全模式](#空安全模式)
11
+ - [内存泄漏](#内存泄漏)
12
+ - [架构:ViewModel 与 Repository](#架构viewmodel-与-repository)
13
+ - [密封类与状态管理](#密封类与状态管理)
14
+ - [Review Checklist](#review-checklist)
15
+
16
+ ---
17
+
18
+ ## 协程:作用域与取消
19
+
20
+ > 📖 通用并发模式和跨语言示例详见 [异步与并发跨语言指南](cross-cutting/async-concurrency-patterns.md)
21
+
22
+ ### 避免 GlobalScope
23
+
24
+ ```kotlin
25
+ // ❌ GlobalScope 生命周期不受控,Activity/Fragment 销毁后协程仍在运行
26
+ GlobalScope.launch {
27
+ val data = api.fetchData()
28
+ binding.textView.text = data.title // Crash: view destroyed
29
+ }
30
+
31
+ // ✅ 使用 viewModelScope,ViewModel 清除时自动取消
32
+ class MyViewModel(private val repo: Repository) : ViewModel() {
33
+ fun loadData() {
34
+ viewModelScope.launch {
35
+ val data = repo.fetchData()
36
+ _uiState.value = UiState.Success(data)
37
+ }
38
+ }
39
+ }
40
+
41
+ // ✅ 在 Activity/Fragment 中使用 lifecycleScope
42
+ class MyActivity : AppCompatActivity() {
43
+ override fun onCreate(savedInstanceState: Bundle?) {
44
+ super.onCreate(savedInstanceState)
45
+ lifecycleScope.launch {
46
+ val data = repo.fetchData()
47
+ binding.textView.text = data.title
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### CancellationException 不能吞掉
54
+
55
+ ```kotlin
56
+ // ❌ 捕获所有异常导致取消信号丢失
57
+ viewModelScope.launch {
58
+ try {
59
+ repo.fetchData()
60
+ } catch (e: Exception) {
61
+ // CancellationException 被吞掉,协程无法取消
62
+ showError(e)
63
+ }
64
+ }
65
+
66
+ // ✅ 重新抛出 CancellationException
67
+ viewModelScope.launch {
68
+ try {
69
+ repo.fetchData()
70
+ } catch (e: CancellationException) {
71
+ throw e // Must rethrow
72
+ } catch (e: Exception) {
73
+ showError(e)
74
+ }
75
+ }
76
+
77
+ // ✅ 或使用 catch 配合 ensureActive
78
+ viewModelScope.launch {
79
+ try {
80
+ repo.fetchData()
81
+ } catch (e: Exception) {
82
+ ensureActive() // Rethrows if cancelled
83
+ showError(e)
84
+ }
85
+ }
86
+ ```
87
+
88
+ ### CPU-bound 任务需要检查取消
89
+
90
+ ```kotlin
91
+ // ❌ CPU 密集计算不响应取消,即使协程已取消也会跑完
92
+ viewModelScope.launch(Dispatchers.Default) {
93
+ for (item in largeList) {
94
+ heavyComputation(item)
95
+ }
96
+ }
97
+
98
+ // ✅ 定期检查 isActive 或调用 ensureActive
99
+ viewModelScope.launch(Dispatchers.Default) {
100
+ for (item in largeList) {
101
+ ensureActive() // Throws CancellationException if cancelled
102
+ heavyComputation(item)
103
+ }
104
+ }
105
+
106
+ // ✅ 或使用 yield 让出执行权
107
+ viewModelScope.launch(Dispatchers.Default) {
108
+ for (item in largeList) {
109
+ yield() // Checks cancellation + yields to other coroutines
110
+ heavyComputation(item)
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### 阻塞操作使用 runInterruptible
116
+
117
+ ```kotlin
118
+ // ❌ 在协程中直接调用阻塞 I/O,阻塞线程池线程
119
+ viewModelScope.launch(Dispatchers.IO) {
120
+ val result = blockingLibraryCall() // Blocks IO thread
121
+ }
122
+
123
+ // ✅ 使用 runInterruptible 包装阻塞调用,支持取消中断
124
+ viewModelScope.launch(Dispatchers.IO) {
125
+ val result = runInterruptible {
126
+ blockingLibraryCall() // Interrupted on cancellation
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### 正确选择调度器
132
+
133
+ ```kotlin
134
+ // ❌ CPU 密集任务用了 IO 调度器(线程池过大,浪费资源)
135
+ viewModelScope.launch(Dispatchers.IO) {
136
+ val bitmap = decodeImage(byteArray) // CPU-bound on IO pool
137
+ }
138
+
139
+ // ✅ CPU 密集用 Default,I/O 操作用 IO
140
+ viewModelScope.launch(Dispatchers.Default) {
141
+ val bitmap = decodeImage(byteArray) // CPU-bound on Default pool
142
+ }
143
+
144
+ // ❌ IO 操作用了 Default 调度器(线程池太小,容易饥饿)
145
+ viewModelScope.launch(Dispatchers.Default) {
146
+ val response = okHttpClient.newCall(request).execute() // I/O on Default pool
147
+ }
148
+
149
+ // ✅ I/O 操作用 IO 调度器
150
+ viewModelScope.launch(Dispatchers.IO) {
151
+ val response = okHttpClient.newCall(request).execute()
152
+ }
153
+ ```
154
+
155
+ ### launch vs async
156
+
157
+ ```kotlin
158
+ // ❌ async 只用于"发个火",不需要返回值
159
+ viewModelScope.launch {
160
+ async { analytics.trackEvent("click") } // Overkill
161
+ }
162
+
163
+ // ✅ 不需要返回值用 launch
164
+ viewModelScope.launch {
165
+ launch { analytics.trackEvent("click") }
166
+ }
167
+
168
+ // ✅ 需要返回值且可能并行时用 async
169
+ viewModelScope.launch {
170
+ val deferredA = async { api.fetchA() }
171
+ val deferredB = async { api.fetchB() }
172
+ val result = combine(deferredA.await(), deferredB.await())
173
+ }
174
+ ```
175
+
176
+ ### 不要用 Job() 破坏父子关系
177
+
178
+ ```kotlin
179
+ // ❌ Job() 切断了父协程的取消传播
180
+ viewModelScope.launch {
181
+ launch(Job()) { // Detached from parent scope!
182
+ importantWork() // Will NOT be cancelled when viewModelScope cancels
183
+ }
184
+ }
185
+
186
+ // ✅ 保持默认的父子关系
187
+ viewModelScope.launch {
188
+ launch { // Child of viewModelScope
189
+ importantWork() // Cancelled when viewModelScope cancels
190
+ }
191
+ }
192
+
193
+ // ✅ 如果确实需要独立生命周期,显式管理并说明原因
194
+ class MyManager(private val scope: CoroutineScope) {
195
+ // Independent lifecycle managed by MyManager.shutdown()
196
+ private val managerJob = Job(scope.coroutineContext[Job])
197
+ private val managerScope = scope + managerJob + Dispatchers.IO
198
+
199
+ fun shutdown() {
200
+ managerJob.cancel()
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### NonCancellable 的正确使用
206
+
207
+ ```kotlin
208
+ // ❌ 整个协程都包在 withContext(NonCancellable) 中,无法取消
209
+ viewModelScope.launch {
210
+ withContext(NonCancellable) { // Entire block is uncancellable!
211
+ val data = repo.fetchData() // Cannot be cancelled
212
+ db.saveData(data) // Cannot be cancelled
213
+ analytics.track("saved")
214
+ }
215
+ }
216
+
217
+ // ✅ NonCancellable 只用于清理操作
218
+ viewModelScope.launch {
219
+ try {
220
+ val data = repo.fetchData()
221
+ db.saveData(data)
222
+ } catch (e: CancellationException) {
223
+ throw e
224
+ } finally {
225
+ withContext(NonCancellable) {
226
+ db.cleanup() // Only cleanup is uncancellable
227
+ }
228
+ }
229
+ }
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Flow 陷阱
235
+
236
+ ### 冷流与热流混淆
237
+
238
+ ```kotlin
239
+ // ❌ 每次 collect 都重新执行 flow {} 块(冷流特性被误解)
240
+ val userFlow = flow {
241
+ emit(api.fetchUser()) // Called once per collector!
242
+ }
243
+
244
+ // Two collectors = two network requests
245
+ lifecycleScope.launch { userFlow.collect { } }
246
+ lifecycleScope.launch { userFlow.collect { } }
247
+
248
+ // ✅ 共享数据用 StateFlow/SharedFlow(热流)
249
+ class MyViewModel(private val repo: Repository) : ViewModel() {
250
+ private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
251
+ val uiState: StateFlow<UiState> = _uiState.asStateFlow()
252
+
253
+ init {
254
+ viewModelScope.launch {
255
+ _uiState.value = UiState.Success(repo.fetchUser())
256
+ }
257
+ }
258
+ }
259
+ // Multiple collectors share the same StateFlow
260
+ ```
261
+
262
+ ### 不要在 flow {} 中切换上下文
263
+
264
+ ```kotlin
265
+ // ❌ 在 flow builder 中使用 withContext,违反约束
266
+ val dataFlow = flow {
267
+ withContext(Dispatchers.IO) { // IllegalStateException!
268
+ emit(api.fetchData())
269
+ }
270
+ }
271
+
272
+ // ✅ 使用 flowOn 操作符切换上游上下文
273
+ val dataFlow = flow {
274
+ emit(api.fetchData()) // Runs on IO via flowOn
275
+ }.flowOn(Dispatchers.IO)
276
+
277
+ // ✅ 或使用 channelFlow / callbackFlow 需要切换时
278
+ val dataFlow = channelFlow {
279
+ withContext(Dispatchers.IO) {
280
+ send(api.fetchData()) // send() is safe in channelFlow
281
+ }
282
+ }
283
+ ```
284
+
285
+ ### collect 需要生命周期感知
286
+
287
+ ```kotlin
288
+ // ❌ 在 Activity/Fragment 中 collect 不感知生命周期
289
+ lifecycleScope.launch {
290
+ viewModel.uiState.collect { state ->
291
+ binding.textView.text = state.title // Crash if view destroyed
292
+ }
293
+ }
294
+
295
+ // ✅ 在 Fragment 中使用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle
296
+ viewLifecycleOwner.lifecycleScope.launch {
297
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
298
+ viewModel.uiState.collect { state ->
299
+ binding.textView.text = state.title
300
+ }
301
+ }
302
+ }
303
+
304
+ // ✅ 在 Compose 中使用 collectAsStateWithLifecycle
305
+ @Composable
306
+ fun MyScreen(viewModel: MyViewModel) {
307
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
308
+ // ...
309
+ }
310
+ ```
311
+
312
+ ### 异常透明性:使用 catch 操作符
313
+
314
+ ```kotlin
315
+ // ❌ 在 collect 中 try-catch 处理上游异常
316
+ viewModelScope.launch {
317
+ try {
318
+ dataFlow.collect { data ->
319
+ processData(data)
320
+ }
321
+ } catch (e: Exception) {
322
+ // This also catches exceptions from processData, not just upstream
323
+ showError(e)
324
+ }
325
+ }
326
+
327
+ // ✅ 使用 catch 操作符保持异常透明性
328
+ viewModelScope.launch {
329
+ dataFlow
330
+ .catch { e -> showError(e) } // Only catches upstream exceptions
331
+ .collect { data ->
332
+ processData(data) // Exceptions here propagate normally
333
+ }
334
+ }
335
+ ```
336
+
337
+ ### StateFlow vs SharedFlow 选择
338
+
339
+ ```kotlin
340
+ // ❌ 用 SharedFlow 模拟 StateFlow,丢失最新值语义
341
+ private val _state = MutableSharedFlow<UiState>()
342
+ val state: SharedFlow<UiState> = _state
343
+
344
+ // ✅ UI 状态用 StateFlow:总是有值、新订阅者立即获得最新值
345
+ class MyViewModel : ViewModel() {
346
+ private val _uiState = MutableStateFlow(UiState.Loading)
347
+ val uiState: StateFlow<UiState> = _uiState.asStateFlow()
348
+ }
349
+
350
+ // ✅ 事件(一次性通知)用 SharedFlow + replay(0)
351
+ class MyViewModel : ViewModel() {
352
+ private val _navigationEvent = MutableSharedFlow<NavTarget>(extraBufferCapacity = 1)
353
+ val navigationEvent: SharedFlow<NavTarget> = _navigationEvent.asSharedFlow()
354
+
355
+ fun navigate(target: NavTarget) {
356
+ _navigationEvent.tryEmit(target)
357
+ }
358
+ }
359
+
360
+ // ✅ Channel 用于一次性事件(替代方案)
361
+ private val _navigationEvent = Channel<NavTarget>(Channel.BUFFERED)
362
+ val navigationEvent = _navigationEvent.receiveAsFlow()
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Jetpack Compose 重组
368
+
369
+ ### 不稳定参数导致多余重组
370
+
371
+ ```kotlin
372
+ // ❌ 使用不稳定的类作为参数,Compose 无法判断是否变化
373
+ data class UserProfile(
374
+ val name: String,
375
+ val friends: List<String>, // Unstable! List is not @Stable
376
+ )
377
+
378
+ @Composable
379
+ fun ProfileCard(profile: UserProfile) { // Recomposes even if profile didn't change
380
+ Text(profile.name)
381
+ }
382
+
383
+ // ✅ 使用 @Immutable 标注或使用稳定的集合类型
384
+ @Immutable
385
+ data class UserProfile(
386
+ val name: String,
387
+ val friends: ImmutableList<String>, // kotlinx.collections.immutable
388
+ )
389
+
390
+ // ✅ 或将不稳定属性提取为单独的参数
391
+ @Composable
392
+ fun ProfileCard(
393
+ name: String, // Stable: String is primitive
394
+ friendCount: Int, // Stable: Int is primitive
395
+ ) {
396
+ Text(name)
397
+ Text("$friendCount friends")
398
+ }
399
+ ```
400
+
401
+ ### Lambda 不稳定与记忆化
402
+
403
+ ```kotlin
404
+ // ❌ 每次重组都创建新的 Lambda,导致子组件不必要的重组
405
+ @Composable
406
+ fun MyScreen(viewModel: MyViewModel) {
407
+ LazyColumn {
408
+ items(items, key = { it.id }) { item ->
409
+ ItemRow(
410
+ item = item,
411
+ onClick = { viewModel.handleClick(item.id) } // New lambda each recomposition!
412
+ )
413
+ }
414
+ }
415
+ }
416
+
417
+ // ✅ 使用 remember 包装 Lambda,或让 ViewModel 暴露稳定回调
418
+ @Composable
419
+ fun MyScreen(viewModel: MyViewModel) {
420
+ LazyColumn {
421
+ items(items, key = { it.id }) { item ->
422
+ ItemRow(
423
+ item = item,
424
+ onClick = remember(item.id) { { viewModel.handleClick(item.id) } }
425
+ )
426
+ }
427
+ }
428
+ }
429
+ ```
430
+
431
+ ### 使用 derivedStateOf 避免高频率重组
432
+
433
+ ```kotlin
434
+ // ❌ 每次滚动都重组整个组件
435
+ @Composable
436
+ fun ScrollToTopButton(lazyListState: LazyListState) {
437
+ val showButton = lazyListState.firstVisibleItemIndex > 0 // Recomposes on every scroll
438
+ if (showButton) {
439
+ Button(onClick = { /* scroll to top */ }) {
440
+ Text("Top")
441
+ }
442
+ }
443
+ }
444
+
445
+ // ✅ 使用 derivedStateOf 只在结果变化时触发重组
446
+ @Composable
447
+ fun ScrollToTopButton(lazyListState: LazyListState) {
448
+ val showButton by remember {
449
+ derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
450
+ }
451
+ if (showButton) {
452
+ Button(onClick = { /* scroll to top */ }) {
453
+ Text("Top")
454
+ }
455
+ }
456
+ }
457
+ ```
458
+
459
+ ### 不要在 Composable 函数体中执行副作用
460
+
461
+ ```kotlin
462
+ // ❌ 在 Composable 函数体中直接触发副作用,每次重组都会执行
463
+ @Composable
464
+ fun MyScreen(userId: String, viewModel: MyViewModel) {
465
+ viewModel.loadUser(userId) // Called on every recomposition!
466
+ val user by viewModel.user.collectAsStateWithLifecycle()
467
+ Text(user?.name ?: "Loading...")
468
+ }
469
+
470
+ // ✅ 使用 LaunchedEffect 在 key 变化时执行副作用
471
+ @Composable
472
+ fun MyScreen(userId: String, viewModel: MyViewModel) {
473
+ LaunchedEffect(userId) {
474
+ viewModel.loadUser(userId) // Only when userId changes
475
+ }
476
+ val user by viewModel.user.collectAsStateWithLifecycle()
477
+ Text(user?.name ?: "Loading...")
478
+ }
479
+
480
+ // ✅ 一次性初始化用 remember { ... }
481
+ @Composable
482
+ fun MyScreen(viewModel: MyViewModel) {
483
+ val initialData = remember { viewModel.getInitialData() }
484
+ }
485
+ ```
486
+
487
+ ### 状态提升
488
+
489
+ ```kotlin
490
+ // ❌ 状态和逻辑耦合在 Composable 内部,无法复用和测试
491
+ @Composable
492
+ fun ToggleButton() {
493
+ var isChecked by remember { mutableStateOf(false) }
494
+ Switch(
495
+ checked = isChecked,
496
+ onCheckedChange = { isChecked = it }
497
+ )
498
+ }
499
+
500
+ // ✅ 状态提升:调用者控制状态
501
+ @Composable
502
+ fun ToggleButton(
503
+ isChecked: Boolean,
504
+ onCheckedChange: (Boolean) -> Unit,
505
+ modifier: Modifier = Modifier,
506
+ ) {
507
+ Switch(
508
+ checked = isChecked,
509
+ onCheckedChange = onCheckedChange,
510
+ modifier = modifier,
511
+ )
512
+ }
513
+
514
+ // ✅ 调用者持有状态
515
+ @Composable
516
+ fun ParentScreen() {
517
+ var enabled by rememberSaveable { mutableStateOf(false) }
518
+ ToggleButton(
519
+ isChecked = enabled,
520
+ onCheckedChange = { enabled = it },
521
+ )
522
+ }
523
+ ```
524
+
525
+ ---
526
+
527
+ ## 空安全模式
528
+
529
+ ### 避免非空断言 !!
530
+
531
+ ```kotlin
532
+ // ❌ 非空断言:如果为 null 直接 NPE
533
+ val user = getUser()!!
534
+ val name = user.name!!
535
+
536
+ // ✅ 安全调用 + 空合并
537
+ val name = getUser()?.name ?: "Unknown"
538
+
539
+ // ✅ requireNotNull 提供有意义的错误信息
540
+ val user = requireNotNull(getUser()) { "User must not be null at this point" }
541
+
542
+ // ✅ 提前返回
543
+ fun process(user: User?) {
544
+ val nonNullUser = user ?: return
545
+ nonNullUser.doSomething()
546
+ }
547
+ ```
548
+
549
+ ### lateinit vs nullable vs lazy
550
+
551
+ ```kotlin
552
+ // ❌ lateinit 用于可能为 null 的值(语义不对)
553
+ lateinit var optionalConfig: Config // Might never be set
554
+
555
+ // ✅ lateinit 用于一定会在使用前初始化的值
556
+ class MyActivity : AppCompatActivity() {
557
+ lateinit var binding: ActivityMainBinding // Set in onCreate
558
+
559
+ override fun onCreate(savedInstanceState: Bundle?) {
560
+ super.onCreate(savedInstanceState)
561
+ binding = ActivityMainBinding.inflate(layoutInflater)
562
+ setContentView(binding.root)
563
+ }
564
+ }
565
+
566
+ // ✅ nullable + lateinit 取决于初始化时机
567
+ // lateinit: 生命周期保证在使用前初始化
568
+ // nullable: 不确定是否初始化,需要 null 检查
569
+ // lazy: 确定在首次访问时初始化
570
+
571
+ class MyViewModel(private val repo: Repository) : ViewModel() {
572
+ // lazy: 首次访问时初始化,线程安全
573
+ val expensiveObject by lazy { ExpensiveObject(repo) }
574
+
575
+ // nullable: 可能不会初始化
576
+ var cachedData: Data? = null
577
+ private set
578
+ }
579
+ ```
580
+
581
+ ### Java 互操作:平台类型泄漏
582
+
583
+ ```kotlin
584
+ // ❌ Java 返回平台类型(可能 null),Kotlin 当作非空使用
585
+ // Java:
586
+ // public User getUser() { return null; }
587
+ val name: String = javaService.getUser().name // NPE!
588
+
589
+ // ✅ 使用可空类型接收 Java 返回值
590
+ val user: User? = javaService.getUser()
591
+ val name = user?.name ?: "Unknown"
592
+
593
+ // ✅ 在 Kotlin 侧包装 Java API,提供安全的类型
594
+ class SafeUserService(private val delegate: JavaUserService) {
595
+ fun getUser(): User? = delegate.getUser() // Explicitly nullable
596
+ }
597
+ ```
598
+
599
+ ---
600
+
601
+ ## 内存泄漏
602
+
603
+ ### 避免在长生命周期协程中捕获 Context/View
604
+
605
+ ```kotlin
606
+ // ❌ 协程捕获了 Activity Context,Activity 销毁后无法回收
607
+ class MyActivity : AppCompatActivity() {
608
+ fun loadData() {
609
+ // Leaking Activity via coroutine
610
+ GlobalScope.launch {
611
+ val data = repo.fetchData()
612
+ // 'this' (Activity) is captured
613
+ binding.textView.text = data // Activity leaked!
614
+ }
615
+ }
616
+ }
617
+
618
+ // ✅ 使用 viewModelScope + 生命周期感知
619
+ class MyViewModel(private val repo: Repository) : ViewModel() {
620
+ private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
621
+ val uiState: StateFlow<UiState> = _uiState.asStateFlow()
622
+
623
+ fun loadData() {
624
+ viewModelScope.launch {
625
+ val data = repo.fetchData()
626
+ _uiState.value = UiState.Success(data) // No Activity reference
627
+ }
628
+ }
629
+ }
630
+ ```
631
+
632
+ ### 注销监听器
633
+
634
+ ```kotlin
635
+ // ❌ 注册监听器但从不注销
636
+ class MyFragment : Fragment() {
637
+ private val sensorListener = object : SensorEventListener {
638
+ override fun onSensorChanged(event: SensorEvent) { }
639
+ override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { }
640
+ }
641
+
642
+ override fun onResume() {
643
+ super.onResume()
644
+ sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_UI)
645
+ // Never unregistered!
646
+ }
647
+ }
648
+
649
+ // ✅ 在 onPause/onDestroyView 中注销
650
+ override fun onResume() {
651
+ super.onResume()
652
+ sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_UI)
653
+ }
654
+
655
+ override fun onPause() {
656
+ super.onPause()
657
+ sensorManager.unregisterListener(sensorListener)
658
+ }
659
+ ```
660
+
661
+ ### 取消自定义 CoroutineScope
662
+
663
+ ```kotlin
664
+ // ❌ 创建 CoroutineScope 但从不取消
665
+ class MyManager(private val scope: CoroutineScope) {
666
+ private val job = SupervisorJob()
667
+ private val managerScope = scope + job + Dispatchers.IO
668
+
669
+ fun start() {
670
+ managerScope.launch {
671
+ while (isActive) {
672
+ pollServer()
673
+ delay(5000)
674
+ }
675
+ }
676
+ }
677
+ // Never cancelled! job lives forever.
678
+ }
679
+
680
+ // ✅ 提供关闭方法并取消 Job
681
+ class MyManager(private val scope: CoroutineScope) {
682
+ private val job = SupervisorJob()
683
+ private val managerScope = scope + job + Dispatchers.IO
684
+
685
+ fun start() {
686
+ managerScope.launch {
687
+ while (isActive) {
688
+ pollServer()
689
+ delay(5000)
690
+ }
691
+ }
692
+ }
693
+
694
+ fun shutdown() {
695
+ job.cancel()
696
+ }
697
+ }
698
+
699
+ // ✅ ViewModel 里直接用内置的 viewModelScope,不用自己管生命周期
700
+ class MyViewModel : ViewModel() {
701
+ private val scope = viewModelScope + Dispatchers.IO
702
+ // Automatically cancelled when ViewModel is cleared
703
+ }
704
+ ```
705
+
706
+ ---
707
+
708
+ ## 架构:ViewModel 与 Repository
709
+
710
+ ### ViewModel 不暴露可变状态
711
+
712
+ ```kotlin
713
+ // ❌ 直接暴露 MutableStateFlow,外部可以随意修改
714
+ class MyViewModel : ViewModel() {
715
+ val uiState = MutableStateFlow<UiState>(UiState.Loading) // Mutable!
716
+
717
+ fun load() {
718
+ viewModelScope.launch {
719
+ uiState.value = UiState.Success(repo.fetchData())
720
+ }
721
+ }
722
+ }
723
+
724
+ // ✅ 暴露不可变接口,内部持有可变版本
725
+ class MyViewModel(private val repo: Repository) : ViewModel() {
726
+ private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
727
+ val uiState: StateFlow<UiState> = _uiState.asStateFlow()
728
+
729
+ fun load() {
730
+ viewModelScope.launch {
731
+ _uiState.value = UiState.Success(repo.fetchData())
732
+ }
733
+ }
734
+ }
735
+ ```
736
+
737
+ ### 业务逻辑下沉到 Repository
738
+
739
+ ```kotlin
740
+ // ❌ ViewModel 中包含数据处理和业务规则逻辑
741
+ class UserViewModel(private val api: Api) : ViewModel() {
742
+ private val _users = MutableStateFlow<List<User>>(emptyList())
743
+ val users: StateFlow<List<User>> = _users.asStateFlow()
744
+
745
+ fun loadUsers() {
746
+ viewModelScope.launch {
747
+ val raw = api.getUsers()
748
+ val filtered = raw.filter { it.isActive }
749
+ val sorted = filtered.sortedBy { it.name.lowercase() }
750
+ val enriched = sorted.map { user ->
751
+ user.copy(displayName = "${user.firstName} ${user.lastName}")
752
+ }
753
+ _users.value = enriched
754
+ }
755
+ }
756
+ }
757
+
758
+ // ✅ ViewModel 只做状态管理,逻辑下沉到 Repository
759
+ class UserRepository(private val api: Api) {
760
+ suspend fun getActiveUsersSorted(): List<User> {
761
+ return api.getUsers()
762
+ .filter { it.isActive }
763
+ .sortedBy { it.name.lowercase() }
764
+ .map { it.copy(displayName = "${it.firstName} ${it.lastName}") }
765
+ }
766
+ }
767
+
768
+ class UserViewModel(private val repo: UserRepository) : ViewModel() {
769
+ private val _users = MutableStateFlow<List<User>>(emptyList())
770
+ val users: StateFlow<List<User>> = _users.asStateFlow()
771
+
772
+ fun loadUsers() {
773
+ viewModelScope.launch {
774
+ _users.value = repo.getActiveUsersSorted()
775
+ }
776
+ }
777
+ }
778
+ ```
779
+
780
+ ### 单一数据源(Offline-First)
781
+
782
+ ```kotlin
783
+ // ❌ ViewModel 直接从网络获取,无缓存,离线不可用
784
+ class MyViewModel(private val api: Api) : ViewModel() {
785
+ fun load() {
786
+ viewModelScope.launch {
787
+ _uiState.value = UiState.Success(api.fetchData())
788
+ }
789
+ }
790
+ }
791
+
792
+ // ✅ Repository 作为单一数据源,先展示本地缓存再更新网络数据
793
+ class MyRepository(
794
+ private val api: Api,
795
+ private val dao: DataDao,
796
+ ) {
797
+ val data: Flow<List<Data>> = dao.getAll()
798
+ .map { entities -> entities.map { it.toDomain() } }
799
+
800
+ suspend fun refresh() {
801
+ val remote = api.fetchData()
802
+ dao.replaceAll(remote.map { it.toEntity() })
803
+ }
804
+ }
805
+
806
+ class MyViewModel(private val repo: MyRepository) : ViewModel() {
807
+ val uiState = repo.data.map { UiState.Success(it) }
808
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState.Loading)
809
+
810
+ fun refresh() {
811
+ viewModelScope.launch { repo.refresh() }
812
+ }
813
+ }
814
+ ```
815
+
816
+ ### Use Case 用于复杂业务逻辑
817
+
818
+ ```kotlin
819
+ // ❌ Repository 方法名变成动词短语,职责膨胀
820
+ class OrderRepository {
821
+ suspend fun validateAndSubmitOrder(order: Order) { }
822
+ suspend fun calculateOrderTotalWithDiscounts(order: Order): Money { }
823
+ suspend fun checkInventoryAndReserve(items: List<Item>) { }
824
+ }
825
+
826
+ // ✅ 使用 Use Case 封装复杂业务逻辑,Repository 只做数据访问
827
+ class SubmitOrderUseCase(
828
+ private val orderRepo: OrderRepository,
829
+ private val inventoryRepo: InventoryRepository,
830
+ private val paymentRepo: PaymentRepository,
831
+ ) {
832
+ suspend operator fun invoke(order: Order): Result<OrderConfirmation> {
833
+ val validated = order.validate()
834
+ inventoryRepo.reserve(validated.items)
835
+ val total = CalculateOrderTotalUseCase().invoke(validated)
836
+ return paymentRepo.charge(total).map { confirmation ->
837
+ orderRepo.save(validated.copy(status = OrderStatus.CONFIRMED))
838
+ confirmation
839
+ }
840
+ }
841
+ }
842
+
843
+ class OrderRepository {
844
+ suspend fun save(order: Order) { }
845
+ suspend fun getById(id: String): Order? { }
846
+ fun observeOrders(): Flow<List<Order>> { }
847
+ }
848
+ ```
849
+
850
+ ---
851
+
852
+ ## 密封类与状态管理
853
+
854
+ ### UI 状态建模:让不可能的状态无法表达
855
+
856
+ ```kotlin
857
+ // ❌ 用 nullable 组合表示状态,可能产生无效组合
858
+ data class UiState(
859
+ val isLoading: Boolean = false,
860
+ val data: List<Item>? = null,
861
+ val error: String? = null,
862
+ )
863
+ // Invalid: isLoading=true AND error != null
864
+ // Invalid: data != null AND error != null
865
+
866
+ // ✅ 使用密封类建模,每种状态互斥
867
+ sealed interface UiState {
868
+ data object Loading : UiState
869
+ data class Success(val data: List<Item>) : UiState
870
+ data class Error(val message: String, val cause: Throwable? = null) : UiState
871
+ }
872
+
873
+ class MyViewModel(private val repo: Repository) : ViewModel() {
874
+ private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
875
+ val uiState: StateFlow<UiState> = _uiState.asStateFlow()
876
+ }
877
+
878
+ // ✅ Compose 中 exhaustive when
879
+ @Composable
880
+ fun MyScreen(viewModel: MyViewModel) {
881
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
882
+ when (state) {
883
+ is UiState.Loading -> CircularProgressIndicator()
884
+ is UiState.Success -> DataList((state as UiState.Success).data)
885
+ is UiState.Error -> ErrorMessage((state as UiState.Error).message)
886
+ }
887
+ }
888
+ ```
889
+
890
+ ### 导航事件建模
891
+
892
+ ```kotlin
893
+ // ❌ 用枚举或字符串表示导航事件,无法携带参数
894
+ sealed class NavEvent {
895
+ object ToDetail : NavEvent()
896
+ object ToSettings : NavEvent()
897
+ }
898
+ // How to pass orderId to ToDetail?
899
+
900
+ // ✅ 密封类携带类型安全参数
901
+ sealed interface NavEvent {
902
+ data class ToDetail(val orderId: String) : NavEvent
903
+ data class ToSettings(val tab: SettingsTab) : NavEvent
904
+ data class ToProfile(val userId: String, val mode: ProfileMode) : NavEvent
905
+ }
906
+
907
+ // ✅ 处理导航事件
908
+ navController.handleNavEvent { event ->
909
+ when (event) {
910
+ is NavEvent.ToDetail -> navController.navigate(DetailRoute(event.orderId))
911
+ is NavEvent.ToSettings -> navController.navigate(SettingsRoute(event.tab))
912
+ is NavEvent.ToProfile -> navController.navigate(ProfileRoute(event.userId, event.mode))
913
+ }
914
+ }
915
+ ```
916
+
917
+ ### 网络结果包装
918
+
919
+ ```kotlin
920
+ // ❌ 用 Result? 或 nullable 表示网络结果,丢失错误信息
921
+ suspend fun fetchUser(id: String): User? {
922
+ return try {
923
+ api.getUser(id)
924
+ } catch (e: Exception) {
925
+ null // What went wrong?
926
+ }
927
+ }
928
+
929
+ // ✅ 使用密封类包装网络结果
930
+ sealed interface NetworkResult<out T> {
931
+ data class Success<T>(val data: T) : NetworkResult<T>
932
+ data class Error(val code: Int, val message: String) : NetworkResult<Nothing>
933
+ data class Exception(val cause: Throwable) : NetworkResult<Nothing>
934
+ }
935
+
936
+ suspend fun fetchUser(id: String): NetworkResult<User> {
937
+ return try {
938
+ val response = api.getUser(id)
939
+ if (response.isSuccessful) {
940
+ NetworkResult.Success(response.body()!!)
941
+ } else {
942
+ NetworkResult.Error(response.code(), response.message())
943
+ }
944
+ } catch (e: Exception) {
945
+ NetworkResult.Exception(e)
946
+ }
947
+ }
948
+
949
+ // ✅ 在 ViewModel 中映射为 UI 状态
950
+ fun loadUser(id: String) {
951
+ viewModelScope.launch {
952
+ when (val result = repo.fetchUser(id)) {
953
+ is NetworkResult.Success -> _uiState.value = UiState.Success(result.data)
954
+ is NetworkResult.Error -> _uiState.value = UiState.Error("Server error: ${result.code}")
955
+ is NetworkResult.Exception -> _uiState.value = UiState.Error(result.cause.message ?: "Unknown")
956
+ }
957
+ }
958
+ }
959
+ ```
960
+
961
+ ---
962
+
963
+ ## Review Checklist
964
+
965
+ ### 协程
966
+
967
+ - [ ] 不使用 `GlobalScope`,使用 `viewModelScope` / `lifecycleScope`
968
+ - [ ] `CancellationException` 被正确重新抛出,未被吞掉
969
+ - [ ] CPU 密集任务使用 `Dispatchers.Default`,I/O 操作使用 `Dispatchers.IO`
970
+ - [ ] 长时间运行的 CPU 任务定期调用 `ensureActive()` 或 `yield()`
971
+ - [ ] 阻塞调用使用 `runInterruptible` 包装
972
+ - [ ] 不使用 `Job()` 破坏父子协程关系
973
+ - [ ] `NonCancellable` 仅用于 `finally` 块中的清理操作
974
+ - [ ] 不需要返回值用 `launch`,需要并行结果用 `async`
975
+
976
+ ### Flow
977
+
978
+ - [ ] 理解冷流(`flow {}`)与热流(`StateFlow`/`SharedFlow`)的区别
979
+ - [ ] 不在 `flow {}` builder 中使用 `withContext`,使用 `flowOn` 操作符
980
+ - [ ] `collect` 配合 `repeatOnLifecycle` 或 `collectAsStateWithLifecycle` 使用
981
+ - [ ] 异常处理使用 `.catch` 操作符而非 `try-catch` 包裹 `collect`
982
+ - [ ] UI 状态用 `StateFlow`,一次性事件用 `SharedFlow` 或 `Channel`
983
+
984
+ ### Compose
985
+
986
+ - [ ] Composable 参数使用稳定类型,避免不必要重组
987
+ - [ ] Lambda 参数使用 `remember` 包装,避免每次重组创建新实例
988
+ - [ ] 派生状态使用 `derivedStateOf` 避免高频率重组
989
+ - [ ] 副作用使用 `LaunchedEffect` / `SideEffect`,不在函数体中直接调用
990
+ - [ ] 状态正确提升(state hoisting),Composable 无状态且可复用
991
+
992
+ ### 空安全
993
+
994
+ - [ ] 不滥用非空断言 `!!`,使用安全调用 `?.` 或空合并 `?:`
995
+ - [ ] `lateinit` 仅用于生命周期保证初始化的属性
996
+ - [ ] Java 互操作返回值使用可空类型接收
997
+ - [ ] `lazy` 用于首次访问时初始化的昂贵对象
998
+
999
+ ### 内存泄漏
1000
+
1001
+ - [ ] 协程不捕获 `Context` / `View` 等短生命周期对象
1002
+ - [ ] 监听器在 `onPause` / `onDestroyView` 中正确注销
1003
+ - [ ] 自定义 `CoroutineScope` 提供取消机制
1004
+ - [ ] 单例不持有 `Activity` / `Fragment` 引用
1005
+
1006
+ ### 架构
1007
+
1008
+ - [ ] ViewModel 不暴露 `MutableStateFlow` / `MutableLiveData`,使用不可变接口
1009
+ - [ ] 业务逻辑下沉到 Repository / Use Case,ViewModel 只做状态管理
1010
+ - [ ] 实现 offline-first:Repository 作为单一数据源
1011
+ - [ ] 复杂业务逻辑封装为独立的 Use Case 类
1012
+
1013
+ ### 密封类与状态
1014
+
1015
+ - [ ] UI 状态使用密封类建模,让不可能的状态无法表达
1016
+ - [ ] 导航事件使用密封类携带类型安全参数
1017
+ - [ ] 网络请求结果使用密封类包装,不丢失错误信息
1018
+ - [ ] `when` 表达式覆盖所有分支(exhaustive check)