1、以声明的方式合并flow
在这一步中,您将对plantsFlow应用排序顺序。我们将使用flow的声明式API执行此操作。
sql
什么是声明式?
声明式是一种API样式,用来描述程序应该做什么,而不是如何做。更为常见的声明式语言之一是SQL,它让开发者能够描述他们希望数据库查询的内容,而不是如何进行查询。
通过使用map、combine或mapLatest等转换,我们能够以声明的方式描述当每个元素在flow中移动时,我们希望如何对其进行转换。借助这种方式,我们甚至能够以声明的方式描述并发,这将极大地简化代码。在本部分中,您将了解如何使用运算符指示Flow启动两个协程,并以声明的方式合并写成结果。
首先,请打开PlantRepository.kt,然后定义一个名为customSortFlow的新私有flow:
PlantRepository.kt
scss
private val customSortFlow = flow { emit(plantsListSortOrderCache.getOrAwait()) }
这可以定义一个Flow,该flow会在被收集时调用getOrAwait,然后emit排序顺序。
由于此flow只发出一个值,因此您还可以使用asFlow直接从getOrAwait函数构建该flow。
java
// Create a flow that calls a single function
private val customSortFlow = plantsListSortOrderCache::getOrAwait.asFlow()
此代码会创建一个新的Flow,该flow调用getOrAwait并将结果作为其第一个和唯一的值发出。具体操作方法是,使用::引用getOrAwait方法,然后对生成的Function对象调用asFlow。
这两个flow执行相同的操作,在完成之前调用getOrAwait并发出结果。
1.1、以声明的方式合并多个flow
现在我们有两个flow,即customSortFlow和plantsFlow,所以让我们以声明的方式合并这两个flow。
将combine运算符添加到plantsFlow:
PlantRepository.kt
kotlin
private val customSortFlow = plantsListSortOrderCache::getOrAwait.asFlow()
val plantsFlow: Flow<List<Plant>>
get() = plantDao.getPlantsFlow()
// When the result of customSortFlow is available,
// this will combine it with the latest value from
// the flow above. Thus, as long as both `plants` and `sortOrder` are have an initial value(their
// flow has emitted at least one value), any change to either `plants` or `sortOrder` will call
// `plants.applySort(sortOrder)`
.combine(customSortFlow) { plants, sortOrder ->
plants.applySort(sortOrder)
}
combine运算符将两个flow合并在一起。两个flow都在自己的协程中运行,然后,每当一个flow生成一个新值,将使用另一个flow中的最新值调用转换。
通过使用combine,我们可以将缓存的网络查询与我们的数据库查询合并。这两个flow将在不同的协程上并发运行。这意味着Room启动网络请求时,Retrofit可以启动网络查询。然后,一旦两个flow的某个结果可用,该函数就会调用combinelambda,我们可以借此对加载的植物应用加载的排序顺序。
css
转换combine将为每个要合并的flow启动一个协程。这样您便可以用并发形式合并两个flow。flow将以一种"公平"的方式合并,这意味着每一个flow都有机会生成值(即使其中一个flow由紧密循环生成)。
如需探索combine运算符的工作方式,请修改customSortFlow,在onStart中发出两次数据(需包含长时间延迟),如下所示:
scss
// Create a flow that calls a single function
private val customSortFlow = plantsListSortOrderCache::getOrAwait.asFlow()
.onStart {
emit(listOf())
delay(1500)
}
当某个观察器先于其他运算符进行监听时,将会发生转换onStart,并发出占位值。因此,我们将发出一个空列表,将getOrAwait调用延迟1500毫秒,然后继续运行原始flow。如果您现在运行应用,您将看到Room数据库查询会立即返回,并与空列表(这意味着其将按字母书序排序)。法乐1500毫秒后,它开始应用自定义排序。
css
在flow运行之前,您可以使用onStart运行挂起代码。该代码甚至可以向flow中emit额外值,因此您可以在网络请求flow中使用它来发出Loading状态。
1.2、flow和主线程安全
Flow可以调用主线程安全 函数,而且它可以保证协程正常的主线程安全性。Room和Retrofit将为我们提供主线程安全性,我们无需执行其他任何操作,即可使用flow发出网络请求或数据库查询。
此flow已使用以下线程:
plantService.customPlantSortOrder在Retrofit线程上运行(调用Call.enqueue)getPlantsFlow将在Room执行器上运行查询applySort将在收集调度程序上运行(在本例中为Dispatchers.Main)
因此,如果我们要做的只是在Retrofit中调用挂起函数并使用Roomflow,就不需要出于主线程安全性考虑而使用这段代码复杂化。
不过,随着数据集的大小增加,调用applySort的速度可能会变慢,最终阻塞主线程。Flow提供了一个名为flowOn的声明式API,用于控制flow在哪个线程上运行。
请将flowOn添加到plantsFlow,如下所示:
PlantRepository.kt
scss
private val customSortFlow = plantsListSortOrderCache::getOrAwait.asFlow()
val plantsFlow: Flow<List<Plant>>
get() = plantDao.getPlantsFlow()
.combine(customSortFlow) { plants, sortOrder ->
plants.applySort(sortOrder)
}
.flowOn(defaultDispatcher)
.conflate()
调用flowOn会对代码的执行方式产生两个重要影响:
- 在
defaultDispatcher上启动新的协程(本例中为DisPatchers.Default),以在调用flowOn之前运行和收集flow。 - 引入一个缓冲区,以将结果从新协程发送给之后的调用。
- 在执行
flowOn之后,将该缓冲区的值发出到Flow。在本例中,这些值即为ViewModel中的asLiveData。
这与withContext切换调度程序的工作方式非常相似,但它会在转换过程中引入一个缓冲区,该缓冲区会改变flow的工作状况。由flowOn启动的协程产生结果的速度要快于调用方耗用结果的速度,所以默认情况下它会缓冲大量结果。
在本例中,我们计划将结果发送到界面,因此我们只关注最新的结果。这就是conflate运算符执行的操作,它会修改flowOn的缓冲区,仅存储上一个结果。如果前一个数据还没有读取,另一个数据就进入,那么前一个数据将被覆盖。
css
运算符flowOn会启动新的协程来收集其中的flow,并引入一个缓冲区写入结果。
您可以使用更多运算符控制缓冲区,例如,conflate表示仅存储缓冲区中生成的最后一个值。
将flowOn与大型对象(如Room的结果)结合使用时必须注意缓冲区,因为很容易占用大量的内存缓冲区。
1.3、运行应用
如果您再次运行应用,您应该会看到您正在加载数据,并使用Flow应用自定义排序顺序。由于我们尚未实现switchMap,因此过滤器选项不会执行任何操作。
2、在两个flow之间切换
如需结束此API的flow版本,请打开PlantListViewModel.kt,根据GrowZone切换flow,就像在LiveData版本中的操作一样。
在plants LiveData中添加以下代码:
PlantListViewModel.kt
swift
private val growZoneFlow = MutableStateFlow<GrowZone>(NoGrowZone)
val plantsUsingFlow: LiveData<List<Plant>> = growZoneFlow.platMapLatest { growZone ->
if (growZone = NoGrowZone) {
plantRepository.plantsFlow
} else {
platRepository.getPlantsWithGrowZoneFlow(growZone)
}
}.asLiveData()
此模式显示了如何将时间(生长区域会有所更改)集成到flow中。这与LiveData.switchMap版本完全相同,也就是根据某个时间在两个数据源之间切换。
2.1、代码解析
PlantListViewModel.kt
ini
private val growZoneFlow = MutableStateFlow<GronZone>(NoGrowZone)
该代码使用NoGrowZone的初始值定义了一个新的MutableStateFlow。这是一个特殊类型的flow值容器,仅保留给与它的最后一个值。它的原始线程安全并发,因此您可以同时从多个线程中写入内容("最新"项优先)。
您还可以进行订阅,获取当前值的更新。总体来说,该代码与LiveData的行为类似:其只保留最后一个值,并允许您观察对其的更改。
css
StateFlow与使用flow{}构建器等创建的常规flow有所不同。StateFlow是使用初始值创建的,即使在没有被收集的情况下,以及在后续收集操作之间,也都保留起状态,您可以使用MutableStateFlow接口更改StateFlow的值(状态)。
通常您会发现,行为与StateFlow类似的这些flow称为热flow,与之相对的是常规冷flow,后者仅在被收集时才执行。
PlantListViewModel.kt
ini
if (growZone == NoGrowZone) {
plantRepository.plantsFlow
} else {
plantRepository.getPlantsWithGrowZoneFlow(growZone)
}
在flatMapLatest内,我们根据growZone进行切换。此代码与LiveData.switchMap版本几乎完全相同,唯一的区别是它返回Flows,而不是LiveDatas。
PlantListViewModel.kt
scss
}.asLiveData()
最后,我们将Flow转换为LiveData,因为我们的Fragment希望从ViewModel公开LiveData。
css
asLiveData运算符会将Flow转换为具有可配置超时的LiveData。与liveData构建器一样,超时将通过旋转使flow保持活动状态,这样您的收集就不会重启。
2.2、更改StateFlow的值
如果需要让应用知道过滤器的更改,我们可以设置MutableStateFlow.value。这样我们可以像这里的操作一样,它可以轻松将事件传递到协程中。
PlantListViewModel.kt
kotlin
fun setGrowZoneNumber(num: Int) {
growZone.value = GrowZone(num)
growZoneFlow.value = GrowZone(num)
launchDataLoad {
plantRepository.tryUpdateRecentPlantsForGrowZoneCache(GrowZone(num))
}
}
fun clearGrowZoneNumber() {
growZone.value = NoGrowZone
growZoneFlow.value = NoGrowZone
launchDataLoad {
plantRepository.tryUpdateRecentPlantsCache()
}
}
2.3、再次运行应用
如果您再次运行应用,过滤器会同时适用于LiveData版本和Flow版本。