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

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) {}
相关推荐
Winston Wood8 分钟前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-3 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen5 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年13 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX13 小时前
kotlin
开发语言·kotlin
建群新人小猿15 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神16 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛16 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法17 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter18 小时前
Android吸顶效果,并有着ViewPager左右切换
android