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会自动清理观察者
我的习惯:在ViewModel里,我通常用_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,我们还需要一个清晰的架构来组织代码。我个人比较推荐单向数据流模式:

UI层 Activity / Fragment ViewModel层 StateFlow + 业务逻辑 Repository层 数据源管理 用户操作 StateFlow更新 请求数据 数据回调 单向数据流:用户操作 → ViewModel → Repository → ViewModel → UI 本地数据库 网络API 内存缓存 Repository统一管理数据源

这个架构的核心思想是:数据只能从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