StateFlow 在 Android 中的应用

使用 Flow 自动更新数据

在 UI 中使用 DAO 获取数据后,如果有人或线程更新了数据库的内容,相应的 UI 不会自动更新。当然你可以编写代码来协调应用程序特定部分的更新,但更好的方式是使用 Flow 来自动监视数据库并自动更新 UI。

首先,需要修改 DAO,Room 的查询支持返回 Flow(注释部分是原来的代码):

@Dao
interface CrimeDao {
    @Query("SELECT * FROM crime")
    // suspend fun getCrimes(): List<Crime>
    fun getCrimes(): Flow<List<Crime>>
}

然后把调用该函数的地方都跟着一起改了:

class CrimeRepository private constructor(context: Context) {
	// suspend fun getCrimes(): List<Crime> = database.crimeDao().getCrimes()
    fun getCrimes(): Flow<List<Crime>> = database.crimeDao().getCrimes()
}

class CrimeListViewModel : ViewModel() {
    // suspend fun loadCrimes(): List<Crime> {
    //     return crimeRepository.getCrimes()
    // }
    val crimes = crimeRepository.getCrimes()
}

最后在原先使用查询结果的地方,改用 collect() 函数来获取结果:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // val crimes = crimeListViewModel.loadCrimes()
        // binding.crimeRecyclerView.adapter = CrimeListAdapter(crimes)
        crimeListViewModel.crimes.collect{ crimes ->
            binding.crimeRecyclerView.adapter = CrimeListAdapter(crimes)
        }
    }
}

这样便可以通过 Flow 来获取数据了。

使用 StateFlow

但是这样使用 Flow 有个问题,就是每当旋转屏幕时,数据会重新查询一次。原先为了防止这个问题,Android 引入了 ViewModel。但现在 ViewModel 里存的是 Flow,而 Flow 是冷流,会在你调用终结操作符(示例里是 collect())时执行代码,而这个终结操作符在 UI 上,UI 是会随着设备配置变化而变化的,因此每当设备配置变化时,UI 跟着变化(重新创建实例),因此终结操作符就重复地执行。

与冷流的特性不同,StateFlow 是个热流,这个“State”表示它本身持有获取到的数据作为状态。数据发生变化后,是直接保存在 StateFlow 维护着的状态中的。因此当你在使用 StateFlow 的数据时,是直接在它的 state 上取值,不会触发额外的查询,这种特性就很适合放在 ViewModel 里当作 ViewModel 的状态,避免了设备配置变化导致的重复查询。

Room 返回的 Flow 是个冷流,要把它转换成 StateFlow,需要新建一个 MutableStateFlow,用它来订阅 Room 返回的 Flow,及时更新自身的状态。同时,将原先暴露的 crimes 由 Room 返回的 Flow 改为新建的 StateFlow。还有些不重要的细枝末节:为了不暴露 crimes 变量的 setter,这里将可读可写的 _crimes 私有化,同时通过暴露一个只读的 crimes_crimes 的值返回。

class CrimeListViewModel : ViewModel() {
    private val _crimes: MutableStateFlow<List<Crime>> = MutableStateFlow(emptyList())
    // val crimes = crimeRepository.getCrimes()
    val crimes: StateFlow<List<Crime>>
        get() = _crimes.asStateFlow()

    init {
        viewModelScope.launch {
            Log.d(TAG, "init. get crimes")
            crimeRepository.getCrimes().collect{ _crimes.value = it }
        }
    }
}

使用 stateIn()

前面的写法比较麻烦,Flow 提供了一个 stateIn() 函数,用于将冷流直接转换成 StateFlow。最终简写为如下代码:

class CrimeListViewModel : ViewModel() {
    private val crimeRepository = CrimeRepository.get()

    val crimes: StateFlow<List<Crime>> = crimeRepository.getCrimes().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = emptyList()
    )
}

stateIn() 有 3 个参数:

  1. scope: CoroutineScope,用于管理 StateFlow 生命周期的协程作用域。当 scope 被取消时,StateFlow 也会停止。

  2. started: SharingStarted,控制 StateFlow 的启动行为。常用的选项有:

    • SharingStarted.Eagerly:立即启动,即使没有收集者。
    • SharingStarted.Lazily:在第一个收集者订阅时启动。
    • SharingStarted.WhileSubscribed(stopTimeoutMillis: Long = 0, replayExpirationMillis: Long = Long.MAX_VALUE):在有收集者时启动,在最后一个收集者取消后等待 stopTimeoutMillis 毫秒后停止。replayExpirationMillis 控制状态值的缓存时间。
  3. initialValue: T,StateFlow 的初始值。在 StateFlow 启动后,如果没有数据发射,收集者会立即收到这个初始值。

@duron600
程序员
calendar_month
加入