17. 状态管理中的模式匹配:用密封类简化复杂逻辑
状态管理,说白了就是处理「当前是什么情况,接下来该干什么」。
我早期做 Android 开发时,习惯用枚举或整数常量来表示状态。比如 STATE_LOADING、STATE_SUCCESS、STATE_ERROR。然后写一堆 if-else 或 when 分支。代码一多,就乱成一锅粥。
后来我接触到 Kotlin 的密封类,才发现——原来状态管理可以这么干净。
17.1 为什么枚举不够用?
枚举能表示「状态的名字」,但带不了「状态的附属数据」。
举个例子:网络请求的结果。
- 加载中:不需要额外数据
- 成功:需要返回的数据
- 失败:需要错误信息、错误码
用枚举,你只能这样写:
enum class UiState {
LOADING,
SUCCESS,
ERROR
}
然后呢?成功的数据放哪?错误信息放哪?
你只能在外面再搞一个 data class 或者用 Any? 强转。我曾经在一个项目里看到这样的代码:
when (state) {
UiState.SUCCESS -> {
val data = extraData as List<Item>
// ...
}
UiState.ERROR -> {
val msg = extraData as String
// ...
}
}
这种写法,说白了就是埋雷。哪天类型不对应,直接崩。
17.2 密封类登场
密封类(sealed class)允许你定义一组有限的子类型。每个子类型可以有自己的属性。
看这个例子:
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String, val code: Int = -1) : UiState<Nothing>()
}
每个状态都自带数据。Loading 不需要数据,Success 带数据,Error 带错误信息。
使用时,配合 when 表达式,编译器会帮你检查是否覆盖了所有分支:
fun render(state: UiState<List<Item>>) {
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showData(state.data)
is UiState.Error -> showError(state.message, state.code)
}
}
注意这里:state.data 是自动推导出来的类型,不需要强转。这就是密封类的威力。
核心优势:密封类把「状态」和「数据」绑定在一起,编译器帮你保证类型安全。
17.3 我在项目中的实际用法
我之前做一个电商 App 的订单列表页。订单状态有十几种:待支付、已支付、已发货、已签收、退款中、已取消……
每个状态需要展示不同的 UI:
- 待支付:显示「去支付」按钮
- 已发货:显示物流信息
- 退款中:显示进度条
如果用枚举 + if-else,那个 Activity 会变成几百行的怪物。
我用密封类重构后,结构清晰多了:
sealed class OrderState {
object PendingPayment : OrderState()
data class Paid(val orderId: String) : OrderState()
data class Shipped(val trackingNumber: String, val company: String) : OrderState()
data class Refunding(val progress: Float) : OrderState()
object Cancelled : OrderState()
}
然后在 ViewHolder 里:
fun bind(state: OrderState) {
when (state) {
is OrderState.PendingPayment -> {
btnPay.visibility = View.VISIBLE
tvStatus.text = "待支付"
}
is OrderState.Shipped -> {
tvTracking.text = "${state.company}: ${state.trackingNumber}"
}
// 其他分支...
}
}
每个分支只处理自己的逻辑。新增一个状态,编译器会提醒你哪里没覆盖到。
小技巧:如果某个状态不需要数据,用 object 而不是 data class。这样不会创建多余的实例。
17.4 密封类 vs 密封接口
Kotlin 1.5 之后引入了 sealed interface。什么时候用哪个?
我个人习惯:
- 如果子类型之间共享一些公共逻辑,用密封类(可以定义构造函数和属性)
- 如果只是纯标记,不需要共享状态,用密封接口
举个例子:
sealed interface NavigationEvent {
object GoBack : NavigationEvent
data class OpenDetail(val id: String) : NavigationEvent
data class ShowToast(val message: String) : NavigationEvent
}
这里不需要公共属性,用接口更轻量。
17.5 避坑指南
我曾经犯过一个错误:在密封类的子类型里使用了 var 属性。
// 不要这样做
data class Error(var message: String) : UiState<Nothing>()
密封类通常用于表示不可变的状态。一旦创建,就不应该修改。用 val 才是正道。
另一个坑:密封类的子类型必须定义在同一个文件里。如果你把子类型分散到多个文件,编译器会报错。
注意:密封类的子类型只能定义在密封类所在的文件内。这是 Kotlin 的强制要求。
17.6 结合 Flow 使用
在 MVVM 架构中,我经常把密封类和 Kotlin Flow 结合:
class OrderViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<Order>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Order>>> = _uiState.asStateFlow()
fun loadOrders() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val orders = repository.getOrders()
_uiState.value = UiState.Success(orders)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
}
Activity 或 Fragment 里收集这个 Flow,然后根据状态渲染 UI。干净、安全、可测试。
17.7 知识体系图
下面这张图展示了密封类在状态管理中的核心位置:
17.8 总结
密封类不是 Kotlin 里最炫酷的特性,但绝对是最实用的之一。
它让状态管理变得可预测、类型安全、易于扩展。你想想看,每次新增一个状态,编译器都会提醒你哪里需要处理。这种安全感,是枚举和 if-else 给不了的。
我个人建议:只要你的状态超过 3 种,或者状态需要携带数据,就优先考虑密封类。别犹豫。
一句话总结:密封类 + when 表达式 = 状态管理的黄金搭档。