1、通过flow进行异步
那么,Flow和Sequence一样是惰性的,但它如何又是异步的呢?我们来看一下异步序列的示例 - 观察对数据库的更改。
在此示例中,我们需要使用另一个线程(如主线程或界面线程)上的观察器来协调数据库线程池上生成的数据。并且,随着数据发生更改,我们将反复发出结果,所以这种情况非常符合异步序列模式。
假设您正在为Flow编写Room集成。如果您从Room中已有的挂起查询支持开始,则可能会编写如下代码:
// This code is a simplified version of how Room implements flow
fun <T> createFlow(query: Query, tables: List<Tables>): Flow<T> = flow {
val changeTracker = tableChangeTracker(tables)
while(true) {
emit(suspendQuery(query))
changeTracker.suspendUntilChanged()
}
}
此代码依靠两个虚构挂起函数生成Flow:
suspendQuery- 该主线程安全函数用于运行常规Room挂起查询suspendUntilChanged- 该函数用于挂起协程,直至其中一个表发生更改。
flow被收集后,最初会emits查询的第一个值。处理该值后,flow将恢复并调用suspendUntilChanged,正如其预期实现的操作一样 - 挂起flow,直至其中一个表发生更改。此时,系统中不会发生任何变化,直至其中一个表发生更改并且flow恢复。
当flow恢复时,系统将执行另一个主线程安全查询并emits结果。这个过程会永远无限循环下去。
1.1、flow和结构化并发
不过,我们不想发生工作泄露。协程本身的资源开销并不高,但它会反复唤醒自身去执行数据库查询。泄露的代价相当高。
虽然我们创建了无限循环,但flow可以通过支持结构化并发帮助我们解决这个问题。
耗用值或对flow进行迭代的唯一方法就是使用终端运算符。因为所有终端运算符都是挂起函数,因此该工作受限于调用它们的作用域的生命周期。该作用域取消后,flow将按照常规协程合作取消规则自动取消。因此,即使我们在flow构建器中编写了无限循环,由于结构化并发,我们仍然可以安全地耗用flow,不会发生泄露。
flow支持结构化并发
由于flow允许您仅通过终端运算符耗用值,因此它可以支持结构化并发。
当flow的使用方被取消时,整个Flow都会被取消。由于结构化并发,中间步骤不可能泄露协程。
2、通过Room使用flow
在此步骤中,您将学会如何在Room中使用Flow,并将其连接到界面。
在Flow的许多用法中,都需要用到此步骤。当按这种方式使用时,Room运算符中的Flow座位类似于LiveData的可观察数据库查询运行。
2.1、更新Dao
首先,请打开PlantDao.kt,然后添加两个返回Flow<List<Plant>>的新查询:
PlantDao.kt
@Query("SELECT * from plants ORDER BY name")
fun getPlantsFlow(): Flow<List<Plant>>
@Query("SELECT * from plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
fun getPlantsWithGrowZoneNumberFLow(growZoneNumber: Int): Flow<List<Plant>>
请注意,除了返回类型之外,这些函数与LiveData版的函数完全相同。但是,我们会同时开发这两种版本,以进行比较。
指定Flow返回类型后,Room执行的查询将具有以下特征:
- 主线程安全 - 具有
Flow返回类型的查询始终在Room执行程序上运行,因此查询始终可在主线程上安全运行。您无需对代码进行任何更改,即可使其在主线程之外运行。 - 观察更改 -
Room会自动观察更改,并将新值发给flow。 - 异步序列 -
Flow会在每次更改时发出完整的查询结果,且不会引入任何缓冲区。如果返回Flow<List<T>>,则flow会发出包含查询结果中所有行的List<T>。flow会像序列一样执行,一次发出一个查询结果并挂起,直至系统向其请求下一个结果。 - 可取消 - 当收集这些flow的作用域被取消时,
Room会取消观察此查询。
以上几项特征相结合,使Flow成为了非常好用的返回类型,用来观察来自界面层的数据路。
2.2、更新代码库
如需继续将新的返回值连接到界面,请打开PlantRepository.kt并添加以下代码:
PlantRepository.kt
val plantsFlow: Flow<List<Plant>>
get() = plantDao.getPlantsFlow()
fun getPlantWithGrowZoneFlow(growZoneNumber: GrowZone): Flow<List<Plant>> {
return plantDao.getPlantsWithGrowZoneNumberFlow(growZoneNumber.number)
}
目前,我们只是将Flow值传递给调用方。
2.3、更新ViewModel
在PlantListViewModel.kt中,让我们先从简单的示例入手,仅公开plantsFlow。在接下来的几个步骤中,我们会在flow版本中添加生长区域切换功能。
PlantListViewModel.kt
// add a new property to plantListViewModel
val plantUsingFlow: LiveData<List<Plant>> = plantRepository.plantsFlow.asLiveData()
我们也会使用LiveData版本(val plants)进行比较
由于我们希望在次界面层中保留LiveData,因此我们将使用asLiveData扩展函数,将Flow转换为LiveData。就像LiveData构建器一样,该函数会为生成的LiveData添加可配置超时。此功能非常有用,因为他会阻止在每次配置更改(例如设备旋转)时重启查询。
asLiveData运算符会将Flow转换为具有可配置超时的LiveData。
与liveData构建器一样,超时将有助于Flow在重启后继续运行。如果在超时之前观察到另一个屏幕,则Flow不会足校。
由于flow提供主线程安全性以及取消功能,因此您可以选择将Flow一直传递到界面层,无需将其转换为LiveData。不过,在此篇博客中,我们将继续在界面层使用LiveData。
同时,在ViewModel中,将缓存更新添加到init代码块中。目前此步骤时可选的,不过如果您清除了缓存但为添加此调用,您将不会在应用中看到任何数据。
PlantListViewModel.kt
init {
clearGrowZoneNumber() // keep this
// fetch the full plant list
launchDataLoad { plantRepository.tryUpdateRecentPlantsCache() }
}
2.4、更新fragment
打开PlantListFragment.kt,将subscribeUi函数更改为使用新的plantsUsingFlow LiveData。
PlantListFragment.kt
private fun subscribeUi(adapter: PlantAdapter) {
viewModel.plantsUsingFlow.observe(viewLifecycleOwner) { plants ->
adapter.submitList(plants)
}
}
2.5、使用flow运行应用
如果您再次运行应用,您应该会看到您现在正在使用Flow加载数据。由于我们尚未实现switchMap,因此过滤器选项不会执行任何操作。