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

1、以声明的方式合并flow

在这一步中,您将对plantsFlow应用排序顺序。我们将使用flow的声明式API执行此操作。

sql 复制代码
什么是声明式?

声明式是一种API样式,用来描述程序应该做什么,而不是如何做。更为常见的声明式语言之一是SQL,它让开发者能够描述他们希望数据库查询的内容,而不是如何进行查询。

通过使用mapcombinemapLatest等转换,我们能够以声明的方式描述当每个元素在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,即customSortFlowplantsFlow,所以让我们以声明的方式合并这两个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可以调用主线程安全 函数,而且它可以保证协程正常的主线程安全性。RoomRetrofit将为我们提供主线程安全性,我们无需执行其他任何操作,即可使用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会对代码的执行方式产生两个重要影响:

  1. defaultDispatcher上启动新的协程(本例中为DisPatchers.Default),以在调用flowOn之前运行和收集flow。
  2. 引入一个缓冲区,以将结果从新协程发送给之后的调用。
  3. 在执行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版本。

相关推荐
深海呐6 分钟前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang7 分钟前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼8 分钟前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss1 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
一丝晨光2 小时前
逻辑运算符
java·c++·python·kotlin·c#·c·逻辑运算符
消失的旧时光-19434 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男5 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽6 小时前
Android 源码集成可卸载 APP
android
码农明明6 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风7 小时前
mariadb主从配置步骤
android·adb·mariadb