1、采用自定义排序的植物
植物列表目前按字母顺序显示,但我们想更改此列表的顺序,先列出某些植物,然后再按字母顺序显示其余的植物。这类似于购物应用在可购买列表的顶部显示赞助商的商品。我们的产品团队希望能够在不发布新版本应用的情况下动态更改排序顺序,因此我们会从后端获取要排在前面的植物列表。
应用自定义排序后的应用如下所示:
自定义顺序排序列表包含以下四种植物:Orange、Sunflower、Gracle和Avocado。请注意它们是如何首先显示在列表中,其后是按字母顺序排序的其余植物。
现在,如果按下过滤器按钮(仅显示GrowZone9的植物),Sunflower会从列表中消失,因为它的GrowZone不是9。自定义排序列表中的其他三种植物都在GrowZone9中,因此它们仍将处于列表顶部。其余植物中只有Tomato在GrowZone9中,因此它显示在此列表的最后。
现在让我们开始编写代码,以实现自定义排序。
2、获取排序顺序
首先,我们编写一个挂起函数,用来从网络中获取自定义排序顺序,然后再将该顺序缓存到内存中。
将以下内容添加到PlantRepository中:
PlantRepository.kt
private var plantsListSortOrderCache = CacheOnSuccess(onErrorFallback = { listOf<String>() }) {
plantService.customPlantSortOrder()
}
plantsListSortOrderCache用作自定义排序顺序的内存缓存。如果出现网络连接错误,这将会退为一个空列表,因此应用即使未获取到排序顺序,也可以显示数据。
此代码使用sunflower模块中的CacheOnSuccess实用程序类来处理缓存。通过像这样抽象出实现缓存的细节,可使应用代码更加直观。由于CacheOnSuccess已通过充分测试,我们无需为了确保操作正确而为代码库编写大量测试。使用kotlinx-coroutines时,最好在代码中引入类似的高级抽象化功能。
现在让我们合并一些逻辑,对植物列表应用排序
将以下内容添加PlantRepository:
PlantRepository.kt
private fun List<Plant>.applySort(customSortOrder: List<String>): List<Plant> {
return sortedBy { plant ->
val positionForItem = customSortOrder.indexOf(plant.plantId).let { order ->
if (order > -1) order else Int.MAX_VALUE
}
ComparablePair(positionForItem, plant.name)
}
}
此扩展函数将重新排列此列表,把customSortOrder中的Plants置于此列表的前面。
3、使用LiveData构建逻辑
现在,排序逻辑已准备就绪,请将plants和getPlantsWithGrowZone的代码替换为下面的LiveData构建器:
PlantRepository.kt
val plants: LiveData<List<Plant>> = liveData<List<Plant>> {
val plantsLiveData = plantDao.getPlants()
val customSortOrder = plantsListSortOrderCache.getOrAwait()
emitSource(plantsLiveData.map {
plantList -> plantList.applySort(customSortOrder)
})
}
fun getPlantsWithGrowZone(growZone: GrowZone) = liveData {
val plantsGrowZoneLiveData = plantDao.getPlantsWithGrowZoneNumber(growZone.number)
val customSortOrder = plantsListSortOrderCache.getOrAwait()
emitSource(plantsGrowZoneLiceData.map { plantList ->
plantList.applySort(customSortOrder)
})
}
现在,如果您运行应用,系统会显示自定义排序的植物列表:
LiveData构建器允许我们以异步方式计算值,因为协程已对liveData进行备份。这里,我们有一个挂起函数用于从数据库中获取植物的LiveData列表,并且还会调用一个挂起函数来获取自定义排序顺序。然后,使用构建器将这两个值合并到一起,对职务列表进行排序并返回值。
每当想要发出新值时,您可以通过调用emitSource()函数,从LiveData发出多个值。请注意,每次调用emitSource()都会移除之前添加的源。
协程在被观察到时开始执行;当协程执行成功,或者数据库或网络调用失败时,协程即被取消。
如果任何挂起函数调用失败,那么整个代码块会被取消,而且不会重启,这样有助于避免泄露。
接下来,我们将借助转换来了解getPlantsWithGrowZone的变体。
4、liveData: 修改值
现在,我们将修改PlantRepository,以便在处理每个值时实现一次挂起转换,以此了解如何在LiveData中构建复杂的一部转换。首先,我们创建一个可在主线程上安全使用的排序算法版本。可以使用withContext切换到另一个仅针对lambda的调度程序,然后恢复使用我们开始时使用的调度程序。
将以下内容添加到PlantRepository中:
PlantRepository
@AnyThread
suspend fun List<Plant>.applyMainSafeSort(customSortOrder: List<String>) = withContext(defaultDispatcher) {
this@applyMainSafeSort.applySort(customSortOrder)
}
在任何调度程序之间切换,协程会使用withContext。调用withContext会切换到仅适用于lambda的另一个调度程序,然后返回到使用该lambda的结果调用它的调度程序。
Kotlin协程默提供三个调度程序:Main、IO和Default。IO调度程序针对IO工作进行了优化。例如从网络或磁盘读取内容,而Default调度则针对CPU密集型任务进行了优化。
然后,我们可以通过LiveData构建器使用这个新的主线程安全排序。更新代码块以使用switchMap,这可让您在每次收到新值时指向新的LiveData。
PlantRepository.kt
fun getPlantWithGrowZone(growZone: GrowZone) = plantDao.getPlantsWithGrowZoneNumber(growZone.number)
.switchMap { plantList ->
liveData {
val customSortOrder = plantsListSortOrderCache.getOrAwait()
emit(plantList.applyMainSafeSort(customSortOrder)
}
}
与先前版本相比,一旦从网络收到自定义排序顺序,此代码即可用于新的主线程安全applyMainSafeSort。然后,系统会将此结果作为getPlantsWithGrowZone返回的新值发送到switchMap。
与上面的plantsLiveData类似,此协程会被观察到时开始执行,并且会在完成时数据库/网络调用失败时终止。这里的区别在于,可以安全的在map中进行网络调用,因为它已经缓存。