9. Android状态管理:从ViewModel到StateFlow
状态管理,说白了就是解决「数据从哪里来、到哪里去、怎么通知界面更新」的问题。我早期做Android开发时,用的还是AsyncTask加回调那一套,代码写起来像意大利面条。后来Google推出了ViewModel和LiveData,情况好了很多。但直到StateFlow出现,我才觉得状态管理真正有了「响应式」的味道。
这一章,我会结合自己的实战经验,带你从ViewModel的基础用法,一路走到StateFlow的进阶玩法。嗯,这里要注意,不是所有场景都适合用StateFlow,我们得搞清楚它的适用边界。
9.1 ViewModel:状态管理的基石
ViewModel的核心职责就两个:管理UI数据和处理生命周期。它不会因为屏幕旋转而被销毁,这让我省了不少心。
先看一个最简单的ViewModel:
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData<Int>(0)
val count: LiveData<Int> get() = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
在Activity或Fragment里,我们这样用:
class CounterFragment : Fragment() {
private val viewModel: CounterViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.count.observe(viewLifecycleOwner) { value ->
textView.text = "Count: $value"
}
button.setOnClickListener { viewModel.increment() }
}
}
这段代码看起来简单,但背后有几个关键点:
- by viewModels():这是Kotlin属性委托,自动帮我们创建或复用ViewModel
- viewLifecycleOwner:用Fragment的视图生命周期,而不是Fragment本身的生命周期。这样可以避免视图销毁后还收到更新
- LiveData的自动解注册:当LifecycleOwner销毁时,LiveData会自动清理观察者
_count作为私有可变数据源,暴露不可变的count给外部。这样能防止外部直接修改数据,保持数据流向清晰。
9.2 LiveData的局限性
LiveData虽然好用,但用久了你会发现一些问题。我在项目中遇到过这样一个场景:需要从两个数据源合并数据,用LiveData实现起来特别别扭。
LiveData的主要痛点:
- 不支持线程切换:LiveData的postValue和setValue在用法上容易混淆
- 数据变换能力弱:虽然有Transformations.map和switchMap,但链式调用一多,代码就变得难以阅读
- 缺少操作符:没有debounce、distinctUntilChanged这些流式操作符
- 粘性事件问题:新观察者会收到之前发送的最新值,这在某些场景下会导致Bug
举个例子,你想想看,如果用户从A页面跳到B页面,B页面观察一个LiveData,结果收到了A页面残留的数据——这就是粘性事件。我曾经因为这个坑排查了半天。
9.3 StateFlow:响应式状态管理的新选择
StateFlow是Kotlin协程库的一部分,它本质上是一个有状态的数据流。和LiveData相比,它有几个明显的优势:
| 特性 | LiveData | StateFlow |
|---|---|---|
| 初始值 | 需要手动设置 | 构造时必填 |
| 线程安全 | 主线程更新 | 任意线程 |
| 操作符 | 有限 | 丰富(map, filter, debounce等) |
| 粘性事件 | 有 | 可通过配置避免 |
| 与协程集成 | 需要额外适配 | 原生支持 |
用StateFlow改写上面的计数器:
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.update { it + 1 }
}
}
在Fragment里收集:
class CounterFragment : Fragment() {
private val viewModel: CounterViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.count.collect { value ->
textView.text = "Count: $value"
}
}
}
}
}
repeatOnLifecycle可以确保在生命周期停止时自动取消协程,避免内存泄漏。这是Google官方推荐的做法。
9.4 StateFlow的高级用法
在实际项目中,状态管理往往不是简单的增删改查。我做过一个电商App,需要同时管理商品列表、购物车、用户登录状态等多个状态。这时候,StateFlow的组合能力就派上用场了。
9.4.1 合并多个StateFlow
class ShoppingViewModel : ViewModel() {
private val _products = MutableStateFlow<List<Product>>(emptyList())
private val _cart = MutableStateFlow<List<CartItem>>(emptyList())
private val _user = MutableStateFlow<User?>(null)
// 合并三个数据流
val uiState: StateFlow<ShoppingUiState> = combine(
_products,
_cart,
_user
) { products, cart, user ->
ShoppingUiState(
products = products,
cartItemCount = cart.sumOf { it.quantity },
isLoggedIn = user != null,
userName = user?.name ?: "未登录"
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ShoppingUiState()
)
}
data class ShoppingUiState(
val products: List<Product> = emptyList(),
val cartItemCount: Int = 0,
val isLoggedIn: Boolean = false,
val userName: String = "未登录"
)
这段代码里,combine把三个数据流合并成一个UI状态。当任何一个数据源变化时,UI会自动更新。而stateIn把冷流转成热流,配合WhileSubscribed(5000),可以在订阅者消失后延迟5秒再停止上游数据流,避免频繁重建。
9.4.2 防抖与去重
搜索功能是StateFlow的典型应用场景。用户输入时,我们不想每次按键都发起网络请求:
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
val searchResults: StateFlow<List<Result>> = _query
.debounce(300) // 300毫秒防抖
.distinctUntilChanged() // 去重,避免重复查询
.flatMapLatest { query ->
if (query.isBlank()) {
flowOf(emptyList())
} else {
repository.search(query)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun onQueryChanged(newQuery: String) {
_query.value = newQuery
}
}
flatMapLatest时,如果前一个网络请求还没完成,新的请求会取消它。这看起来是好事,但如果你需要保留前一个请求的结果(比如分页加载),就得改用flatMapConcat或自己管理协程。
9.5 状态管理的架构设计
有了ViewModel和StateFlow,我们还需要一个清晰的架构来组织代码。我个人比较推荐单向数据流模式:
这个架构的核心思想是:数据只能从Repository流向ViewModel,再流向UI。UI只能通过事件(比如点击、输入)反向影响ViewModel,不能直接修改数据。
9.6 实战:一个完整的登录流程
最后,我们用一个登录功能来串联所有知识点。这个例子我在实际项目中用过类似的模式:
// 登录状态
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Success(val user: User) : LoginState()
data class Error(val message: String) : LoginState()
}
// ViewModel
class LoginViewModel(
private val loginRepository: LoginRepository
) : ViewModel() {
private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
val loginState: StateFlow<LoginState> = _loginState.asStateFlow()
private val _username = MutableStateFlow("")
val username: StateFlow<String> = _username.asStateFlow()
private val _password = MutableStateFlow("")
val password: StateFlow<String> = _password.asStateFlow()
// 登录按钮是否可用
val isLoginEnabled: StateFlow<Boolean> = combine(
_username, _password
) { username, password ->
username.isNotBlank() && password.length >= 6
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false
)
fun onUsernameChanged(value: String) {
_username.value = value
}
fun onPasswordChanged(value: String) {
_password.value = value
}
fun login() {
val username = _username.value
val password = _password.value
if (username.isBlank() || password.length < 6) return
viewModelScope.launch {
_loginState.value = LoginState.Loading
try {
val user = loginRepository.login(username, password)
_loginState.value = LoginState.Success(user)
} catch (e: Exception) {
_loginState.value = LoginState.Error(e.message ?: "登录失败")
}
}
}
}
在UI层收集状态时,我们可以用when表达式来优雅地处理所有分支:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.loginState.collect { state ->
when (state) {
is LoginState.Idle -> { /* 显示初始界面 */ }
is LoginState.Loading -> { /* 显示加载动画 */ }
is LoginState.Success -> { /* 跳转到主页 */ }
is LoginState.Error -> { /* 显示错误提示 */ }
}
}
}
}
when分支,不会漏掉任何状态。这在多人协作的项目中特别有用。
从ViewModel到StateFlow,这条路我走了好几年。刚开始觉得LiveData够用了,后来发现StateFlow的灵活性和协程生态的整合才是未来。但话说回来,技术选型没有银弹。如果你的项目比较简单,LiveData完全够用。如果涉及到复杂的异步操作和数据变换,StateFlow会是更好的选择。
嗯,这一章的内容就到这里。记住,状态管理的核心不是用哪个库,而是数据流向清晰、状态可预测、代码可维护。工具只是手段,设计思想才是根本。
公众号:蓝海资料掘金营,微信deep3321