1、以声明的方式合并flow
在这一步中,您将对plantsFlow
应用排序顺序。我们将使用flow的声明式API执行此操作。
sql
什么是声明式?
声明式是一种API样式,用来描述程序应该做什么,而不是如何做。更为常见的声明式语言之一是SQL,它让开发者能够描述他们希望数据库查询的内容,而不是如何进行查询。
通过使用map
、combine
或mapLatest
等转换,我们能够以声明的方式描述当每个元素在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,即customSortFlow
和plantsFlow
,所以让我们以声明的方式合并这两个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的某个结果可用,该函数就会调用combine
lambda,我们可以借此对加载的植物应用加载的排序顺序。
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
可以调用主线程安全 函数,而且它可以保证协程正常的主线程安全性。Room
和Retrofit
将为我们提供主线程安全性,我们无需执行其他任何操作,即可使用flow发出网络请求或数据库查询。
此flow已使用以下线程:
plantService.customPlantSortOrder
在Retrofit线程上运行(调用Call.enqueue
)getPlantsFlow
将在Room执行器上运行查询applySort
将在收集调度程序上运行(在本例中为Dispatchers.Main
)
因此,如果我们要做的只是在Retrofit
中调用挂起函数并使用Room
flow,就不需要出于主线程安全性考虑而使用这段代码复杂化。
不过,随着数据集的大小增加,调用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
会对代码的执行方式产生两个重要影响:
- 在
defaultDispatcher
上启动新的协程(本例中为DisPatchers.Default
),以在调用flowOn
之前运行和收集flow。 - 引入一个缓冲区,以将结果从新协程发送给之后的调用。
- 在执行
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
版本。