1、将样式与flow混合
Flow
中最令人兴奋的一项功能是它为挂起函数提供绝佳的支持。flow
构建器以及几乎每一个转换都会公开一个可调用任何挂起函数的suspend
运算符。因此,可以通过从flow内部调用常规挂起函数来确保网路哦和数据库调用的主线程安全性,以及安排多个异步操作。
实际上,这允许您自然地将生命式转换与命令式代码混合使用。如本例中所示,在常规map运算符内,您可以安排多个异步操作,而无需应用任何额外的转换。在很多地方,与完全声明式相比,使用这种方式的代码更简单。
css
如果你大量使用RxJava等库,这也将是Flow与其他库的主要区别之一。
开始使用Flow时,请仔细考虑如何使用挂起转换简化代码。在许多情况下,您可以依靠map、onStart和onCompletion等运算符中的挂起操作,自然地表达异步代码。
来自Rx的比较熟悉的运算符(例如combine、mapLatest、flatMapLatest、flattenMerge和flatMapMerge)最适合用于在Flow中安排并发操作。
1.1、使用挂起函数安排异步工作
接下来我们将对Flow
进行总结。使用挂起运算符实现自定义排序。
打开PlantRepository.kt
并向getPlantsWithGrowZoneNumberFlow
添加map转换。
PlantReposotory.kt
kotlin
fun getPlantsWithGrowZoneFlow(growZone: GrowZone): Flow<List<Plant>> {
return plantDao.getPlantsWithGrowZoneNumberFlow(growZone.number)
.map { plantList ->
val sortOrderFromNetwork = plantsListSortOrderCache.getOrAwait()
val nextValue = plantList.applyMainSafeSort(sortOrderFromNetwork)
nextValue
}
}
此map依赖常规挂起函数处理异步工作,因此具有主线程安全性,即便它合并了两项一步操作。
从数据库中返回每项结果后,我们会获得缓存的排序顺序。如果该排序未准备好,它将等待异步网络请求。一旦我们有了排序顺序,您就可以安全地调用applyMainSafeSort
,它将在默认调度程序上运行该排序。
现在,该代码将主线程安全问题迁移到了常规挂起函数中,因此已经完全具备主线程安全性。这比在plantsFlow
中实现的相同转换简单得多。
css
在Flow中,map和其他运算符接受挂起lambda。
通过使用协程的挂起和恢复机制,您通常可以轻松安排依序异步调用,而无需使用声明式转换。
从调用挂起转换的协程之外的其他协程发出值时错误操作。
如果您在一个flow操作中启动另一个协程(就像我们在getOrAwait和applyMainSafeSort内所做的那样),请确保在emitting该值之前将它返回到原始协程。
不过值得注意的是,执行的方式略有不同。每当数据库发出新值,都会获取缓存值。这没有问题,因为我们将在plantsListSortOrderCache
中正确缓存该值,但如果启动了新的网络请求,此实现将产生许多不必要的网络请求。此外,在.combine
版本中,网络请求和数据库查询会并发运行,而在此版本中,将按序列运行。
由于存在这些差异,因此构建此代码并没有明确的规则。在许多情况下,可以像我们在这里的操作一样使用挂起转换,这样可以使所有异步操作按顺序进行。但在其他情况下,最好使用运算符来控制并发以及提供主线程安全性。
2、使用flow控制并发
我们将网络请求移至基于flow的协程中。
这样,我们便可从onClick
调用的处理程序中移除进行网络调用的逻辑,并从growZone
驱动它们。这可帮助我们创建单一可信来源并避免代码重复-在没有刷新缓存时,任何代码都无法更改过滤器。
打开PlantListViewModel.kt
,然后将它添加到init代码块:
PlantListViewModel.kt
ini
init {
clearGrowZoneNumber()
growZone.mapLatest { growZone ->
_spinner.value = true
if (growZone == NoGrowZone) {
plantRepository.tryUpdateRecentPlantsCache()
} else {
plantRepository.tryUpdateRecentPlantsForGrowZoneCache(growZone)
}
}
.onEach { _spinner.value = false }
.catch { throwable -> _snackbar.value = throwable.message }
.launchIn(viewModelScope)
}
此代码将启动一个新协程,观察噶送到growZoneChannel
的值。您现在可通过以下方法把网络调用注释掉,因为它们只适用于LiveData
版本。
PlantListViewModel.kt
scss
funn getGrowZoneNumber(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.1、再次运行应用
如果您现在再次运行应用,您会看到网络刷新现在由growZone
控制。我们对代码进行了实质性的改进,因为作为过滤条件激活的唯一真实来源,渠道中有了更多更改过滤器的方法。这样一来,网络请求和当前过滤器便不可避免地需要同步。
2.2、代码解析
让我们从外部开始,注意解析一下代码中所用的所有新函数:
PlantListViewModel.kt
scss
growZone
// ...
.launchIn(viewModelScope)
这一次,我们使用launchIn
运算符在ViewModel
内收集flow。
运算符launchIn
创建了一个新协程,并从flow中收集每个值。它将在所提供的CoroutineScope
中启动(本例中为viewModelScope
)。非常棒,因为此ViewModel
会被清除,收集将被取消。
如果没有提供任何其他运算符,那这样做不会有太大作用,但是由于Flow
会在其所有运算符中提供挂起lambda,所以您可以根据每个值轻松执行异步操作。
css
使用flow时,通常会根据需要收集ViewModel、Repository或其他数据层中的数据。
由于Flow没有与界面关联,因此您不需要界面观察器来collect一个flow。这与LiveData有很大不同,LiveData始终需要运行界面观察器。最好不要尝试在您的ViewModel中observe一个LiveData,因为它没有适合的观察生命周期。
PlantLiveViewModel.kt
ini
.mapLatest { growZone ->
_spinner.value = true
if (growZone == NoGrowZone) {
plantRepository.tryUpdateRecentPlantsCache()
} else {
plantRepository.tryUpdateRecentPlantsForGrowZoneCache(growZone)
}
}
这就是神奇之处 - mapLatest
回味每个值使用此map函数。但是与常规map
不同,每次调用map转换它都会启动新的协程。然后,如果growZoneChannel
在上一个协程完成之前发出了新值,它会在启动新协程之前取消该新值。
我们可以使用mapLatest
控制并发。不用我们自己手动构建取消/重启逻辑,flow转换可以解决此问题。与手动编写相同的取消逻辑相比,此代码更简洁和易于理解。
取消Flow
遵循正常的协程合作取消规则。
python
如果您以前使用过RxJava,则可以像使用switchMap一样使用mapLatest。
主要区别在于,它在一个新的协程中提供挂起lambda,因此您可以直接从mapLatest调用常规挂起函数。
PlantListViewModel.kt
ini
.onEach { _spinner.value = false }
.catch { throwable -> _snackbar.value = throwable.message }
每当上面的flow发出一个值时,onEach
就会被调用。在本示例中,我们将在处理完成后使用它来重置旋转图标。
catch
运算符将捕获flow中针对其抛出的所有异常。它可以向flow发出一个新值(例如错误状态),将异常重新跑如flow,或像我们在这里所做的操作一样。
如果发生错误,我们只需告知_spinnerbar
显示错误消息即可。
2.3、总结
此步骤展示了是如何使用Flow
控制并发,以及如何使用ViewModel
中的Flows
,而无需依赖于界面观察器。
作为一个具有挑战性的步骤,请尝试定义一个函数来封装此flow的数据加载,使用以下签名:
kotlin
fun <T> loadDataFor(source: StateFlow<T>, block: suspend(T) -> Unit) {}