28 KiB
28 KiB
Kotlin / Android Code Review Guide
Kotlin/Android 代码审查指南,覆盖协程作用域与取消、Flow 陷阱、Compose 重组、空安全、内存泄漏、架构分层与密封类状态建模等核心主题。
目录
协程:作用域与取消
避免 GlobalScope
// ❌ GlobalScope 生命周期不受控,Activity/Fragment 销毁后协程仍在运行
GlobalScope.launch {
val data = api.fetchData()
binding.textView.text = data.title // Crash: view destroyed
}
// ✅ 使用 viewModelScope,ViewModel 清除时自动取消
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 不能吞掉
// ❌ 捕获所有异常导致取消信号丢失
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 任务需要检查取消
// ❌ 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
// ❌ 在协程中直接调用阻塞 I/O,阻塞线程池线程
viewModelScope.launch(Dispatchers.IO) {
val result = blockingLibraryCall() // Blocks IO thread
}
// ✅ 使用 runInterruptible 包装阻塞调用,支持取消中断
viewModelScope.launch(Dispatchers.IO) {
val result = runInterruptible {
blockingLibraryCall() // Interrupted on cancellation
}
}
正确选择调度器
// ❌ CPU 密集任务用了 IO 调度器(线程池过大,浪费资源)
viewModelScope.launch(Dispatchers.IO) {
val bitmap = decodeImage(byteArray) // CPU-bound on IO pool
}
// ✅ CPU 密集用 Default,I/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
// ❌ 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() 破坏父子关系
// ❌ 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 的正确使用
// ❌ 整个协程都包在 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 陷阱
冷流与热流混淆
// ❌ 每次 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 {} 中切换上下文
// ❌ 在 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 需要生命周期感知
// ❌ 在 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 操作符
// ❌ 在 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 选择
// ❌ 用 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 重组
不稳定参数导致多余重组
// ❌ 使用不稳定的类作为参数,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 不稳定与记忆化
// ❌ 每次重组都创建新的 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 避免高频率重组
// ❌ 每次滚动都重组整个组件
@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 函数体中执行副作用
// ❌ 在 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() }
}
状态提升
// ❌ 状态和逻辑耦合在 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 },
)
}
空安全模式
避免非空断言 !!
// ❌ 非空断言:如果为 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
// ❌ 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 互操作:平台类型泄漏
// ❌ 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
// ❌ 协程捕获了 Activity Context,Activity 销毁后无法回收
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
}
}
}
注销监听器
// ❌ 注册监听器但从不注销
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
// ❌ 创建 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 不暴露可变状态
// ❌ 直接暴露 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
// ❌ 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)
// ❌ 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 用于复杂业务逻辑
// ❌ 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 状态建模:让不可能的状态无法表达
// ❌ 用 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)
}
}
导航事件建模
// ❌ 用枚举或字符串表示导航事件,无法携带参数
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))
}
}
网络结果包装
// ❌ 用 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 Case,ViewModel 只做状态管理
- 实现 offline-first:Repository 作为单一数据源
- 复杂业务逻辑封装为独立的 Use Case 类
密封类与状态
- UI 状态使用密封类建模,让不可能的状态无法表达
- 导航事件使用密封类携带类型安全参数
- 网络请求结果使用密封类包装,不丢失错误信息
when表达式覆盖所有分支(exhaustive check)