Files

1017 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kotlin / Android Code Review Guide
> Kotlin/Android 代码审查指南,覆盖协程作用域与取消、Flow 陷阱、Compose 重组、空安全、内存泄漏、架构分层与密封类状态建模等核心主题。
## 目录
- [协程:作用域与取消](#协程作用域与取消)
- [Flow 陷阱](#flow-陷阱)
- [Jetpack Compose 重组](#jetpack-compose-重组)
- [空安全模式](#空安全模式)
- [内存泄漏](#内存泄漏)
- [架构:ViewModel 与 Repository](#架构viewmodel-与-repository)
- [密封类与状态管理](#密封类与状态管理)
- [Review Checklist](#review-checklist)
---
## 协程:作用域与取消
### 避免 GlobalScope
```kotlin
// ❌ GlobalScope 生命周期不受控,Activity/Fragment 销毁后协程仍在运行
GlobalScope.launch {
val data = api.fetchData()
binding.textView.text = data.title // Crash: view destroyed
}
// ✅ 使用 viewModelScopeViewModel 清除时自动取消
class MyViewModel(private val repo: Repository) : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = repo.fetchData()
_uiState.value = UiState.Success(data)
}
}
}
// ✅ 在 Activity/Fragment 中使用 lifecycleScope
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val data = repo.fetchData()
binding.textView.text = data.title
}
}
}
```
### CancellationException 不能吞掉
```kotlin
// ❌ 捕获所有异常导致取消信号丢失
viewModelScope.launch {
try {
repo.fetchData()
} catch (e: Exception) {
// CancellationException 被吞掉,协程无法取消
showError(e)
}
}
// ✅ 重新抛出 CancellationException
viewModelScope.launch {
try {
repo.fetchData()
} catch (e: CancellationException) {
throw e // Must rethrow
} catch (e: Exception) {
showError(e)
}
}
// ✅ 或使用 catch 配合 ensureActive
viewModelScope.launch {
try {
repo.fetchData()
} catch (e: Exception) {
ensureActive() // Rethrows if cancelled
showError(e)
}
}
```
### CPU-bound 任务需要检查取消
```kotlin
// ❌ CPU 密集计算不响应取消,即使协程已取消也会跑完
viewModelScope.launch(Dispatchers.Default) {
for (item in largeList) {
heavyComputation(item)
}
}
// ✅ 定期检查 isActive 或调用 ensureActive
viewModelScope.launch(Dispatchers.Default) {
for (item in largeList) {
ensureActive() // Throws CancellationException if cancelled
heavyComputation(item)
}
}
// ✅ 或使用 yield 让出执行权
viewModelScope.launch(Dispatchers.Default) {
for (item in largeList) {
yield() // Checks cancellation + yields to other coroutines
heavyComputation(item)
}
}
```
### 阻塞操作使用 runInterruptible
```kotlin
// ❌ 在协程中直接调用阻塞 I/O,阻塞线程池线程
viewModelScope.launch(Dispatchers.IO) {
val result = blockingLibraryCall() // Blocks IO thread
}
// ✅ 使用 runInterruptible 包装阻塞调用,支持取消中断
viewModelScope.launch(Dispatchers.IO) {
val result = runInterruptible {
blockingLibraryCall() // Interrupted on cancellation
}
}
```
### 正确选择调度器
```kotlin
// ❌ CPU 密集任务用了 IO 调度器(线程池过大,浪费资源)
viewModelScope.launch(Dispatchers.IO) {
val bitmap = decodeImage(byteArray) // CPU-bound on IO pool
}
// ✅ CPU 密集用 DefaultI/O 操作用 IO
viewModelScope.launch(Dispatchers.Default) {
val bitmap = decodeImage(byteArray) // CPU-bound on Default pool
}
// ❌ IO 操作用了 Default 调度器(线程池太小,容易饥饿)
viewModelScope.launch(Dispatchers.Default) {
val response = okHttpClient.newCall(request).execute() // I/O on Default pool
}
// ✅ I/O 操作用 IO 调度器
viewModelScope.launch(Dispatchers.IO) {
val response = okHttpClient.newCall(request).execute()
}
```
### launch vs async
```kotlin
// ❌ async 只用于"发个火",不需要返回值
viewModelScope.launch {
async { analytics.trackEvent("click") } // Overkill
}
// ✅ 不需要返回值用 launch
viewModelScope.launch {
launch { analytics.trackEvent("click") }
}
// ✅ 需要返回值且可能并行时用 async
viewModelScope.launch {
val deferredA = async { api.fetchA() }
val deferredB = async { api.fetchB() }
val result = combine(deferredA.await(), deferredB.await())
}
```
### 不要用 Job() 破坏父子关系
```kotlin
// ❌ Job() 切断了父协程的取消传播
viewModelScope.launch {
launch(Job()) { // Detached from parent scope!
importantWork() // Will NOT be cancelled when viewModelScope cancels
}
}
// ✅ 保持默认的父子关系
viewModelScope.launch {
launch { // Child of viewModelScope
importantWork() // Cancelled when viewModelScope cancels
}
}
// ✅ 如果确实需要独立生命周期,显式管理并说明原因
class MyManager(private val scope: CoroutineScope) {
// Independent lifecycle managed by MyManager.shutdown()
private val managerJob = Job(scope.coroutineContext[Job])
private val managerScope = scope + managerJob + Dispatchers.IO
fun shutdown() {
managerJob.cancel()
}
}
```
### NonCancellable 的正确使用
```kotlin
// ❌ 整个协程都包在 withContext(NonCancellable) 中,无法取消
viewModelScope.launch {
withContext(NonCancellable) { // Entire block is uncancellable!
val data = repo.fetchData() // Cannot be cancelled
db.saveData(data) // Cannot be cancelled
analytics.track("saved")
}
}
// ✅ NonCancellable 只用于清理操作
viewModelScope.launch {
try {
val data = repo.fetchData()
db.saveData(data)
} catch (e: CancellationException) {
throw e
} finally {
withContext(NonCancellable) {
db.cleanup() // Only cleanup is uncancellable
}
}
}
```
---
## Flow 陷阱
### 冷流与热流混淆
```kotlin
// ❌ 每次 collect 都重新执行 flow {} 块(冷流特性被误解)
val userFlow = flow {
emit(api.fetchUser()) // Called once per collector!
}
// Two collectors = two network requests
lifecycleScope.launch { userFlow.collect { } }
lifecycleScope.launch { userFlow.collect { } }
// ✅ 共享数据用 StateFlow/SharedFlow(热流)
class MyViewModel(private val repo: Repository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
_uiState.value = UiState.Success(repo.fetchUser())
}
}
}
// Multiple collectors share the same StateFlow
```
### 不要在 flow {} 中切换上下文
```kotlin
// ❌ 在 flow builder 中使用 withContext,违反约束
val dataFlow = flow {
withContext(Dispatchers.IO) { // IllegalStateException!
emit(api.fetchData())
}
}
// ✅ 使用 flowOn 操作符切换上游上下文
val dataFlow = flow {
emit(api.fetchData()) // Runs on IO via flowOn
}.flowOn(Dispatchers.IO)
// ✅ 或使用 channelFlow / callbackFlow 需要切换时
val dataFlow = channelFlow {
withContext(Dispatchers.IO) {
send(api.fetchData()) // send() is safe in channelFlow
}
}
```
### collect 需要生命周期感知
```kotlin
// ❌ 在 Activity/Fragment 中 collect 不感知生命周期
lifecycleScope.launch {
viewModel.uiState.collect { state ->
binding.textView.text = state.title // Crash if view destroyed
}
}
// ✅ 在 Fragment 中使用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
binding.textView.text = state.title
}
}
}
// ✅ 在 Compose 中使用 collectAsStateWithLifecycle
@Composable
fun MyScreen(viewModel: MyViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// ...
}
```
### 异常透明性:使用 catch 操作符
```kotlin
// ❌ 在 collect 中 try-catch 处理上游异常
viewModelScope.launch {
try {
dataFlow.collect { data ->
processData(data)
}
} catch (e: Exception) {
// This also catches exceptions from processData, not just upstream
showError(e)
}
}
// ✅ 使用 catch 操作符保持异常透明性
viewModelScope.launch {
dataFlow
.catch { e -> showError(e) } // Only catches upstream exceptions
.collect { data ->
processData(data) // Exceptions here propagate normally
}
}
```
### StateFlow vs SharedFlow 选择
```kotlin
// ❌ 用 SharedFlow 模拟 StateFlow,丢失最新值语义
private val _state = MutableSharedFlow<UiState>()
val state: SharedFlow<UiState> = _state
// ✅ UI 状态用 StateFlow:总是有值、新订阅者立即获得最新值
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
// ✅ 事件(一次性通知)用 SharedFlow + replay(0)
class MyViewModel : ViewModel() {
private val _navigationEvent = MutableSharedFlow<NavTarget>(extraBufferCapacity = 1)
val navigationEvent: SharedFlow<NavTarget> = _navigationEvent.asSharedFlow()
fun navigate(target: NavTarget) {
_navigationEvent.tryEmit(target)
}
}
// ✅ Channel 用于一次性事件(替代方案)
private val _navigationEvent = Channel<NavTarget>(Channel.BUFFERED)
val navigationEvent = _navigationEvent.receiveAsFlow()
```
---
## Jetpack Compose 重组
### 不稳定参数导致多余重组
```kotlin
// ❌ 使用不稳定的类作为参数,Compose 无法判断是否变化
data class UserProfile(
val name: String,
val friends: List<String>, // Unstable! List is not @Stable
)
@Composable
fun ProfileCard(profile: UserProfile) { // Recomposes even if profile didn't change
Text(profile.name)
}
// ✅ 使用 @Immutable 标注或使用稳定的集合类型
@Immutable
data class UserProfile(
val name: String,
val friends: ImmutableList<String>, // kotlinx.collections.immutable
)
// ✅ 或将不稳定属性提取为单独的参数
@Composable
fun ProfileCard(
name: String, // Stable: String is primitive
friendCount: Int, // Stable: Int is primitive
) {
Text(name)
Text("$friendCount friends")
}
```
### Lambda 不稳定与记忆化
```kotlin
// ❌ 每次重组都创建新的 Lambda,导致子组件不必要的重组
@Composable
fun MyScreen(viewModel: MyViewModel) {
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(
item = item,
onClick = { viewModel.handleClick(item.id) } // New lambda each recomposition!
)
}
}
}
// ✅ 使用 remember 包装 Lambda,或让 ViewModel 暴露稳定回调
@Composable
fun MyScreen(viewModel: MyViewModel) {
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(
item = item,
onClick = remember(item.id) { { viewModel.handleClick(item.id) } }
)
}
}
}
```
### 使用 derivedStateOf 避免高频率重组
```kotlin
// ❌ 每次滚动都重组整个组件
@Composable
fun ScrollToTopButton(lazyListState: LazyListState) {
val showButton = lazyListState.firstVisibleItemIndex > 0 // Recomposes on every scroll
if (showButton) {
Button(onClick = { /* scroll to top */ }) {
Text("Top")
}
}
}
// ✅ 使用 derivedStateOf 只在结果变化时触发重组
@Composable
fun ScrollToTopButton(lazyListState: LazyListState) {
val showButton by remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
}
if (showButton) {
Button(onClick = { /* scroll to top */ }) {
Text("Top")
}
}
}
```
### 不要在 Composable 函数体中执行副作用
```kotlin
// ❌ 在 Composable 函数体中直接触发副作用,每次重组都会执行
@Composable
fun MyScreen(userId: String, viewModel: MyViewModel) {
viewModel.loadUser(userId) // Called on every recomposition!
val user by viewModel.user.collectAsStateWithLifecycle()
Text(user?.name ?: "Loading...")
}
// ✅ 使用 LaunchedEffect 在 key 变化时执行副作用
@Composable
fun MyScreen(userId: String, viewModel: MyViewModel) {
LaunchedEffect(userId) {
viewModel.loadUser(userId) // Only when userId changes
}
val user by viewModel.user.collectAsStateWithLifecycle()
Text(user?.name ?: "Loading...")
}
// ✅ 一次性初始化用 remember { ... }
@Composable
fun MyScreen(viewModel: MyViewModel) {
val initialData = remember { viewModel.getInitialData() }
}
```
### 状态提升
```kotlin
// ❌ 状态和逻辑耦合在 Composable 内部,无法复用和测试
@Composable
fun ToggleButton() {
var isChecked by remember { mutableStateOf(false) }
Switch(
checked = isChecked,
onCheckedChange = { isChecked = it }
)
}
// ✅ 状态提升:调用者控制状态
@Composable
fun ToggleButton(
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Switch(
checked = isChecked,
onCheckedChange = onCheckedChange,
modifier = modifier,
)
}
// ✅ 调用者持有状态
@Composable
fun ParentScreen() {
var enabled by rememberSaveable { mutableStateOf(false) }
ToggleButton(
isChecked = enabled,
onCheckedChange = { enabled = it },
)
}
```
---
## 空安全模式
### 避免非空断言 !!
```kotlin
// ❌ 非空断言:如果为 null 直接 NPE
val user = getUser()!!
val name = user.name!!
// ✅ 安全调用 + 空合并
val name = getUser()?.name ?: "Unknown"
// ✅ requireNotNull 提供有意义的错误信息
val user = requireNotNull(getUser()) { "User must not be null at this point" }
// ✅ 提前返回
fun process(user: User?) {
val nonNullUser = user ?: return
nonNullUser.doSomething()
}
```
### lateinit vs nullable vs lazy
```kotlin
// ❌ lateinit 用于可能为 null 的值(语义不对)
lateinit var optionalConfig: Config // Might never be set
// ✅ lateinit 用于一定会在使用前初始化的值
class MyActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding // Set in onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
// ✅ nullable + lateinit 取决于初始化时机
// lateinit: 生命周期保证在使用前初始化
// nullable: 不确定是否初始化,需要 null 检查
// lazy: 确定在首次访问时初始化
class MyViewModel(private val repo: Repository) : ViewModel() {
// lazy: 首次访问时初始化,线程安全
val expensiveObject by lazy { ExpensiveObject(repo) }
// nullable: 可能不会初始化
var cachedData: Data? = null
private set
}
```
### Java 互操作:平台类型泄漏
```kotlin
// ❌ Java 返回平台类型(可能 null),Kotlin 当作非空使用
// Java:
// public User getUser() { return null; }
val name: String = javaService.getUser().name // NPE!
// ✅ 使用可空类型接收 Java 返回值
val user: User? = javaService.getUser()
val name = user?.name ?: "Unknown"
// ✅ 在 Kotlin 侧包装 Java API,提供安全的类型
class SafeUserService(private val delegate: JavaUserService) {
fun getUser(): User? = delegate.getUser() // Explicitly nullable
}
```
---
## 内存泄漏
### 避免在长生命周期协程中捕获 Context/View
```kotlin
// ❌ 协程捕获了 Activity ContextActivity 销毁后无法回收
class MyActivity : AppCompatActivity() {
fun loadData() {
// Leaking Activity via coroutine
GlobalScope.launch {
val data = repo.fetchData()
// 'this' (Activity) is captured
binding.textView.text = data // Activity leaked!
}
}
}
// ✅ 使用 viewModelScope + 生命周期感知
class MyViewModel(private val repo: Repository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadData() {
viewModelScope.launch {
val data = repo.fetchData()
_uiState.value = UiState.Success(data) // No Activity reference
}
}
}
```
### 注销监听器
```kotlin
// ❌ 注册监听器但从不注销
class MyFragment : Fragment() {
private val sensorListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) { }
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { }
}
override fun onResume() {
super.onResume()
sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_UI)
// Never unregistered!
}
}
// ✅ 在 onPause/onDestroyView 中注销
override fun onResume() {
super.onResume()
sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_UI)
}
override fun onPause() {
super.onPause()
sensorManager.unregisterListener(sensorListener)
}
```
### 取消自定义 CoroutineScope
```kotlin
// ❌ 创建 CoroutineScope 但从不取消
class MyManager(private val scope: CoroutineScope) {
private val job = SupervisorJob()
private val managerScope = scope + job + Dispatchers.IO
fun start() {
managerScope.launch {
while (isActive) {
pollServer()
delay(5000)
}
}
}
// Never cancelled! job lives forever.
}
// ✅ 提供关闭方法并取消 Job
class MyManager(private val scope: CoroutineScope) {
private val job = SupervisorJob()
private val managerScope = scope + job + Dispatchers.IO
fun start() {
managerScope.launch {
while (isActive) {
pollServer()
delay(5000)
}
}
}
fun shutdown() {
job.cancel()
}
}
// ✅ ViewModel 里直接用内置的 viewModelScope,不用自己管生命周期
class MyViewModel : ViewModel() {
private val scope = viewModelScope + Dispatchers.IO
// Automatically cancelled when ViewModel is cleared
}
```
---
## 架构:ViewModel 与 Repository
### ViewModel 不暴露可变状态
```kotlin
// ❌ 直接暴露 MutableStateFlow,外部可以随意修改
class MyViewModel : ViewModel() {
val uiState = MutableStateFlow<UiState>(UiState.Loading) // Mutable!
fun load() {
viewModelScope.launch {
uiState.value = UiState.Success(repo.fetchData())
}
}
}
// ✅ 暴露不可变接口,内部持有可变版本
class MyViewModel(private val repo: Repository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun load() {
viewModelScope.launch {
_uiState.value = UiState.Success(repo.fetchData())
}
}
}
```
### 业务逻辑下沉到 Repository
```kotlin
// ❌ ViewModel 中包含数据处理和业务规则逻辑
class UserViewModel(private val api: Api) : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
fun loadUsers() {
viewModelScope.launch {
val raw = api.getUsers()
val filtered = raw.filter { it.isActive }
val sorted = filtered.sortedBy { it.name.lowercase() }
val enriched = sorted.map { user ->
user.copy(displayName = "${user.firstName} ${user.lastName}")
}
_users.value = enriched
}
}
}
// ✅ ViewModel 只做状态管理,逻辑下沉到 Repository
class UserRepository(private val api: Api) {
suspend fun getActiveUsersSorted(): List<User> {
return api.getUsers()
.filter { it.isActive }
.sortedBy { it.name.lowercase() }
.map { it.copy(displayName = "${it.firstName} ${it.lastName}") }
}
}
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
fun loadUsers() {
viewModelScope.launch {
_users.value = repo.getActiveUsersSorted()
}
}
}
```
### 单一数据源(Offline-First
```kotlin
// ❌ ViewModel 直接从网络获取,无缓存,离线不可用
class MyViewModel(private val api: Api) : ViewModel() {
fun load() {
viewModelScope.launch {
_uiState.value = UiState.Success(api.fetchData())
}
}
}
// ✅ Repository 作为单一数据源,先展示本地缓存再更新网络数据
class MyRepository(
private val api: Api,
private val dao: DataDao,
) {
val data: Flow<List<Data>> = dao.getAll()
.map { entities -> entities.map { it.toDomain() } }
suspend fun refresh() {
val remote = api.fetchData()
dao.replaceAll(remote.map { it.toEntity() })
}
}
class MyViewModel(private val repo: MyRepository) : ViewModel() {
val uiState = repo.data.map { UiState.Success(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState.Loading)
fun refresh() {
viewModelScope.launch { repo.refresh() }
}
}
```
### Use Case 用于复杂业务逻辑
```kotlin
// ❌ Repository 方法名变成动词短语,职责膨胀
class OrderRepository {
suspend fun validateAndSubmitOrder(order: Order) { }
suspend fun calculateOrderTotalWithDiscounts(order: Order): Money { }
suspend fun checkInventoryAndReserve(items: List<Item>) { }
}
// ✅ 使用 Use Case 封装复杂业务逻辑,Repository 只做数据访问
class SubmitOrderUseCase(
private val orderRepo: OrderRepository,
private val inventoryRepo: InventoryRepository,
private val paymentRepo: PaymentRepository,
) {
suspend operator fun invoke(order: Order): Result<OrderConfirmation> {
val validated = order.validate()
inventoryRepo.reserve(validated.items)
val total = CalculateOrderTotalUseCase().invoke(validated)
return paymentRepo.charge(total).map { confirmation ->
orderRepo.save(validated.copy(status = OrderStatus.CONFIRMED))
confirmation
}
}
}
class OrderRepository {
suspend fun save(order: Order) { }
suspend fun getById(id: String): Order? { }
fun observeOrders(): Flow<List<Order>> { }
}
```
---
## 密封类与状态管理
### UI 状态建模:让不可能的状态无法表达
```kotlin
// ❌ 用 nullable 组合表示状态,可能产生无效组合
data class UiState(
val isLoading: Boolean = false,
val data: List<Item>? = null,
val error: String? = null,
)
// Invalid: isLoading=true AND error != null
// Invalid: data != null AND error != null
// ✅ 使用密封类建模,每种状态互斥
sealed interface UiState {
data object Loading : UiState
data class Success(val data: List<Item>) : UiState
data class Error(val message: String, val cause: Throwable? = null) : UiState
}
class MyViewModel(private val repo: Repository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
// ✅ Compose 中 exhaustive when
@Composable
fun MyScreen(viewModel: MyViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
when (state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> DataList((state as UiState.Success).data)
is UiState.Error -> ErrorMessage((state as UiState.Error).message)
}
}
```
### 导航事件建模
```kotlin
// ❌ 用枚举或字符串表示导航事件,无法携带参数
sealed class NavEvent {
object ToDetail : NavEvent()
object ToSettings : NavEvent()
}
// How to pass orderId to ToDetail?
// ✅ 密封类携带类型安全参数
sealed interface NavEvent {
data class ToDetail(val orderId: String) : NavEvent
data class ToSettings(val tab: SettingsTab) : NavEvent
data class ToProfile(val userId: String, val mode: ProfileMode) : NavEvent
}
// ✅ 处理导航事件
navController.handleNavEvent { event ->
when (event) {
is NavEvent.ToDetail -> navController.navigate(DetailRoute(event.orderId))
is NavEvent.ToSettings -> navController.navigate(SettingsRoute(event.tab))
is NavEvent.ToProfile -> navController.navigate(ProfileRoute(event.userId, event.mode))
}
}
```
### 网络结果包装
```kotlin
// ❌ 用 Result? 或 nullable 表示网络结果,丢失错误信息
suspend fun fetchUser(id: String): User? {
return try {
api.getUser(id)
} catch (e: Exception) {
null // What went wrong?
}
}
// ✅ 使用密封类包装网络结果
sealed interface NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>
data class Error(val code: Int, val message: String) : NetworkResult<Nothing>
data class Exception(val cause: Throwable) : NetworkResult<Nothing>
}
suspend fun fetchUser(id: String): NetworkResult<User> {
return try {
val response = api.getUser(id)
if (response.isSuccessful) {
NetworkResult.Success(response.body()!!)
} else {
NetworkResult.Error(response.code(), response.message())
}
} catch (e: Exception) {
NetworkResult.Exception(e)
}
}
// ✅ 在 ViewModel 中映射为 UI 状态
fun loadUser(id: String) {
viewModelScope.launch {
when (val result = repo.fetchUser(id)) {
is NetworkResult.Success -> _uiState.value = UiState.Success(result.data)
is NetworkResult.Error -> _uiState.value = UiState.Error("Server error: ${result.code}")
is NetworkResult.Exception -> _uiState.value = UiState.Error(result.cause.message ?: "Unknown")
}
}
}
```
---
## Review Checklist
### 协程
- [ ] 不使用 `GlobalScope`,使用 `viewModelScope` / `lifecycleScope`
- [ ] `CancellationException` 被正确重新抛出,未被吞掉
- [ ] CPU 密集任务使用 `Dispatchers.Default`I/O 操作使用 `Dispatchers.IO`
- [ ] 长时间运行的 CPU 任务定期调用 `ensureActive()``yield()`
- [ ] 阻塞调用使用 `runInterruptible` 包装
- [ ] 不使用 `Job()` 破坏父子协程关系
- [ ] `NonCancellable` 仅用于 `finally` 块中的清理操作
- [ ] 不需要返回值用 `launch`,需要并行结果用 `async`
### Flow
- [ ] 理解冷流(`flow {}`)与热流(`StateFlow`/`SharedFlow`)的区别
- [ ] 不在 `flow {}` builder 中使用 `withContext`,使用 `flowOn` 操作符
- [ ] `collect` 配合 `repeatOnLifecycle``collectAsStateWithLifecycle` 使用
- [ ] 异常处理使用 `.catch` 操作符而非 `try-catch` 包裹 `collect`
- [ ] UI 状态用 `StateFlow`,一次性事件用 `SharedFlow``Channel`
### Compose
- [ ] Composable 参数使用稳定类型,避免不必要重组
- [ ] Lambda 参数使用 `remember` 包装,避免每次重组创建新实例
- [ ] 派生状态使用 `derivedStateOf` 避免高频率重组
- [ ] 副作用使用 `LaunchedEffect` / `SideEffect`,不在函数体中直接调用
- [ ] 状态正确提升(state hoisting),Composable 无状态且可复用
### 空安全
- [ ] 不滥用非空断言 `!!`,使用安全调用 `?.` 或空合并 `?:`
- [ ] `lateinit` 仅用于生命周期保证初始化的属性
- [ ] Java 互操作返回值使用可空类型接收
- [ ] `lazy` 用于首次访问时初始化的昂贵对象
### 内存泄漏
- [ ] 协程不捕获 `Context` / `View` 等短生命周期对象
- [ ] 监听器在 `onPause` / `onDestroyView` 中正确注销
- [ ] 自定义 `CoroutineScope` 提供取消机制
- [ ] 单例不持有 `Activity` / `Fragment` 引用
### 架构
- [ ] ViewModel 不暴露 `MutableStateFlow` / `MutableLiveData`,使用不可变接口
- [ ] 业务逻辑下沉到 Repository / Use CaseViewModel 只做状态管理
- [ ] 实现 offline-firstRepository 作为单一数据源
- [ ] 复杂业务逻辑封装为独立的 Use Case 类
### 密封类与状态
- [ ] UI 状态使用密封类建模,让不可能的状态无法表达
- [ ] 导航事件使用密封类携带类型安全参数
- [ ] 网络请求结果使用密封类包装,不丢失错误信息
- [ ] `when` 表达式覆盖所有分支(exhaustive check