1. 状态管理困境:为什么我们需要更好的方案?
说实话,我做了快八年的Android开发,踩过最多的坑就是状态管理。
你想想看,一个App里有多少状态?用户登录没、列表加载完没、弹窗显示不显示、表单填到哪一步了……这些看似简单的东西,一旦项目变大,就会变成一团乱麻。
我记得刚入行那会儿,带我的老大哥跟我说了一句话,我现在还记得:「代码烂不烂,看状态管理就知道。」
1.1 传统方式:那些年我们写过的面条代码
最早的时候,Android里根本没有所谓的状态管理方案。大家怎么干?直接往Activity里塞变量。
class LoginActivity : AppCompatActivity() {
private var isLoading = false
private var isLoggedIn = false
private var errorMessage: String? = null
private var username = ""
private var password = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 各种逻辑混在一起
}
private fun login() {
isLoading = true
updateUI()
// 网络请求...
}
}
这种写法有什么问题?我简单列几条:
- 状态散落一地——变量到处定义,改一个漏三个
- 生命周期耦合——Activity一重建,状态全丢
- 测试基本没戏——想测个登录逻辑?你得先跑起整个Activity
我在一个电商项目里就吃过这个亏。当时有个购物车页面,状态变量写了二十多个。后来产品说要加个「凑单满减」功能,我改了两天,改出三个bug。嗯,那感觉,真酸爽。
1.2 为什么传统方式撑不住了?
说白了,传统方式的核心问题就一个:状态和UI绑得太死。
你想想看,Activity既要管生命周期,又要管业务逻辑,还要管UI刷新。一个类几千行,谁看了不头疼?
我后来总结了一下,传统方式有三大硬伤:
| 问题 | 具体表现 | 后果 |
|---|---|---|
| 状态分散 | 变量散落在Activity、Fragment、Adapter里 | 改一处,崩三处 |
| 生命周期敏感 | 旋转屏幕、切后台都会导致状态丢失 | 用户体验极差 |
| 难以测试 | 业务逻辑和UI代码混在一起 | 单元测试覆盖率接近0 |
1.3 现代架构的演进:我们到底在追求什么?
后来大家开始反思:能不能把状态从UI里抽出来?
于是就有了MVP、MVVM、MVI这些架构模式。它们的核心思路其实都一样:让状态独立存在,UI只管订阅和展示。
我个人习惯用MVVM,因为它和Android的Jetpack配合得最好。你看ViewModel加上LiveData或者StateFlow,基本就能解决大部分问题。
但这里有个新问题:状态本身怎么定义?
用普通类?用接口?用枚举?
我试过很多种方式。用普通类吧,容易写出各种非法状态。比如一个登录页面,既可以是「加载中」,又可以是「已登录」,这两个状态同时存在就出问题了。
用枚举吧,又太死板。你没法在枚举里附带额外信息,比如错误码、用户数据什么的。
1.4 状态管理的核心矛盾
说白了,状态管理要解决的就是三个问题:
- 状态怎么定义——既要精确,又要灵活
- 状态怎么流转——谁改的?什么时候改的?
- 状态怎么消费——UI怎么知道状态变了?
传统方式三个都没解决好。现代架构解决了后两个,但第一个——状态定义——依然是个难题。
你想想看,一个网络请求的状态,可能有:空闲、加载中、成功、失败。如果用普通类,你可能会写出这样的代码:
data class UiState(
val isLoading: Boolean = false,
val isSuccess: Boolean = false,
val isError: Boolean = false,
val errorMessage: String? = null,
val data: List<Item>? = null
)
看着还行对吧?但问题来了:isLoading = true 和 isSuccess = true 同时为 true 怎么办?这种非法状态,编译器根本不会管你。
我在一个金融项目里就遇到过这种事。同事写了个状态类,加载中和成功两个标志位同时为true,结果UI同时显示了加载动画和成功提示——用户一脸懵。
1.5 从混乱到有序:一张图看懂演进
下面这张图,是我自己总结的状态管理演进路线。你看一眼就能明白,我们为什么要追求更好的方案。
1.6 密封类:一个被低估的解决方案
看到这里你可能会问:那到底用什么来定义状态?
我的答案是:密封类(Sealed Class)。
为什么?因为它能让你把状态定义得既精确又安全。每个状态是一个独立的子类,互斥的状态根本不可能同时存在。
举个例子:
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Success(val user: User) : LoginState()
data class Error(val code: Int, val message: String) : LoginState()
}
你看,Loading 和 Success 是互斥的。编译器保证你不可能同时处于两个状态。这就是密封类的威力。
我在一个重构项目里用了密封类之后,状态相关的bug直接降了80%。不是夸张,是真的。
好了,这一章我们聊了为什么需要更好的状态管理方案。下一章,我会带你深入密封类的细节,看看它到底怎么用、用在哪儿、有哪些坑。
公众号:蓝海资料掘金营,微信deep3321