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版本。

相关推荐
SRC_BLUE_1729 分钟前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封5 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4875 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛5 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫6 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水8 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器