Kotlin Flow和LiveData的高级协程(第四趴)

1,294 阅读5分钟

1、通过flow进行异步

那么,FlowSequence一样是惰性的,但它如何又是异步的呢?我们来看一下异步序列的示例 - 观察对数据库的更改。

在此示例中,我们需要使用另一个线程(如主线程或界面线程)上的观察器来协调数据库线程池上生成的数据。并且,随着数据发生更改,我们将反复发出结果,所以这种情况非常符合异步序列模式。

假设您正在为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,因此过滤器选项不会执行任何操作。