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表达式里漏了处理。

核心优势:编译期检查 + 类型安全 + 无需else兜底

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 ?: "未知错误")
    }
}
我的经验:用密封类管理网络状态后,UI层再也不需要判断"数据是否为null"这种逻辑了。每个状态的数据结构都是明确的,Success一定有数据,Error一定有异常。

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 知识体系总览

下面这张图总结了密封类在状态管理中的核心逻辑:

密封类 + when 表达式:状态管理核心逻辑 密封类 UiState object Loading data class Success(data) data class Error(code, msg) object Empty when 表达式 is Loading → showLoading() is Success → showData(data) is Error → handleError() is Empty → showEmpty() 编译器检查 ✅ 所有分支覆盖 ✅ 类型安全 ✅ 无需else兜底 ❌ 遗漏分支报错 结论:密封类 + when = 编译期安全的状态管理,告别运行时if-else地狱

4.8 总结

密封类+when表达式,说白了就是用编译器的力量帮你管理状态。你不需要再写一堆if-else来猜测"这个状态有没有被处理",编译器会直接告诉你。我个人觉得,这是Kotlin在Android状态管理领域最实用的特性之一。

记住三个要点:

  • 密封类定义有限状态集,每个子类携带自己的数据
  • when表达式穷举所有分支,编译器检查是否遗漏
  • 新增状态时编译器强制更新,避免运行时错误

下次写状态管理的时候,试试用密封类替代if-else。你会发现代码清晰很多,bug也少了很多。


公众号:蓝海资料掘金营,微信deep3321