4. 密封类的实战:用when表达式告别if-else地狱
说实话,我见过太多Android项目里,状态管理写着写着就变成了if-else的深坑。你想想看,一个网络请求,要处理加载中、成功、失败、空数据……每个状态都来一个if判断,代码很快就变成一团乱麻。今天我们就用密封类+when表达式,把这团乱麻彻底理清楚。
4.1 为什么if-else会变成地狱?
先看一个我接手过的真实项目。当时有个网络请求的UI状态管理,代码大概是这样的:
// 反面教材:if-else地狱
fun handleResult(result: Any) {
if (result is Loading) {
showLoading()
} else if (result is Success) {
if (result.data != null) {
showData(result.data)
} else {
showEmpty()
}
} else if (result is Error) {
if (result.code == 401) {
redirectToLogin()
} else {
showError(result.message)
}
} else {
// 这里居然还有兜底逻辑
showUnknown()
}
}
这段代码有什么问题?可读性差、容易遗漏分支、扩展困难。后来加了个Refresh状态,结果忘了在else里处理,线上直接崩了。嗯,这就是if-else地狱的典型症状。
4.2 密封类:给状态一个明确的身份
密封类说白了就是有限子类的枚举加强版。每个子类都是一个独立的状态,编译器会帮你检查所有分支是否都处理了。
// 定义密封类
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val code: Int, val message: String) : UiState()
object Empty : UiState()
}
我个人习惯把UI状态定义成密封类,因为这样类型安全。每个状态可以携带不同的数据,比如Success带数据列表,Error带错误码和消息。这在if-else里很难优雅实现。
4.3 when表达式:编译器帮你兜底
有了密封类,when表达式就成了最佳搭档。看这段代码:
fun renderState(state: UiState) {
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showData(state.data)
is UiState.Error -> handleError(state.code, state.message)
is UiState.Empty -> showEmpty()
// 不需要else分支!编译器知道所有情况都覆盖了
}
}
注意看,这里没有else分支。为什么?因为密封类限制了UiState只有这四个子类,编译器能静态检查到所有分支都已覆盖。如果你新增一个UiState.Refresh子类,编译器会直接报错,提醒你when表达式里漏了处理。
4.4 实战:网络请求状态管理
我在项目中经常用密封类管理网络请求的完整生命周期。来看一个更完整的例子:
// 定义网络请求状态
sealed class NetworkState<out T> {
object Idle : NetworkState<Nothing>()
object Loading : NetworkState<Nothing>()
data class Success<T>(val data: T) : NetworkState<T>()
data class Error(val exception: Throwable) : NetworkState<Nothing>()
}
// ViewModel中使用
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow<NetworkState<User>>(NetworkState.Idle)
val state: StateFlow<NetworkState<User>> = _state.asStateFlow()
fun loadUser() {
viewModelScope.launch {
_state.value = NetworkState.Loading
try {
val user = api.fetchUser()
_state.value = NetworkState.Success(user)
} catch (e: Exception) {
_state.value = NetworkState.Error(e)
}
}
}
}
// UI层处理
fun renderUserState(state: NetworkState<User>) {
when (state) {
is NetworkState.Idle -> { /* 初始状态,什么都不做 */ }
is NetworkState.Loading -> showProgressBar()
is NetworkState.Success -> showUserInfo(state.data)
is NetworkState.Error -> showErrorSnackbar(state.exception.message ?: "未知错误")
}
}
4.5 避坑指南:我曾经踩过的坑
我曾经在项目里犯过一个低级错误——在密封类里用了抽象类而不是密封类。抽象类虽然也能定义子类,但编译器不会检查分支完整性。结果就是:新增了一个子类,when表达式没更新,运行时直接抛异常。
还有一次,我在密封类里混入了普通类作为子类。密封类的子类必须定义在同一个文件里,或者嵌套在密封类内部。如果你把子类定义到别的文件,编译器会报错。这个限制其实是个好事,它保证了所有状态都在你的掌控之中。
4.6 进阶:带参数的密封类
有时候状态需要携带更复杂的参数。比如分页加载,需要知道当前页码和是否还有更多数据:
sealed class PaginatedState<T> {
object Idle : PaginatedState<Nothing>()
data class Loading<T>(val page: Int) : PaginatedState<T>()
data class Success<T>(
val data: List<T>,
val page: Int,
val hasMore: Boolean
) : PaginatedState<T>()
data class Error(val message: String) : PaginatedState<Nothing>()
}
// 使用示例
fun renderPaginatedState(state: PaginatedState<String>) {
when (state) {
is PaginatedState.Idle -> initList()
is PaginatedState.Loading -> {
if (state.page == 1) showFullLoading()
else showLoadMoreIndicator()
}
is PaginatedState.Success -> {
updateList(state.data)
if (!state.hasMore) hideLoadMore()
}
is PaginatedState.Error -> showErrorToast(state.message)
}
}
你看,每个状态携带的参数都是类型安全的。Loading状态知道当前加载的是第几页,Success状态知道数据列表和是否还有更多。这在if-else里根本做不到这么清晰。
4.7 知识体系总览
下面这张图总结了密封类在状态管理中的核心逻辑:
4.8 总结
密封类+when表达式,说白了就是用编译器的力量帮你管理状态。你不需要再写一堆if-else来猜测"这个状态有没有被处理",编译器会直接告诉你。我个人觉得,这是Kotlin在Android状态管理领域最实用的特性之一。
记住三个要点:
- 密封类定义有限状态集,每个子类携带自己的数据
- when表达式穷举所有分支,编译器检查是否遗漏
- 新增状态时编译器强制更新,避免运行时错误
下次写状态管理的时候,试试用密封类替代if-else。你会发现代码清晰很多,bug也少了很多。
公众号:蓝海资料掘金营,微信deep3321