我想使用kotlin流从rest端点异步加载多个数据,并在回收器视图中显示它。数据定义为ServiceDto
,ProductDto
,CategoryDto
。ProductDto有一个categoryId
属性,它引用一个CategoryDto。
每个DTO都应该异步加载和显示,但CategoryDto必须在ProductDto之前完成,因为我需要类别名称+依赖的产品并将其添加到mutableListOf<Any>
。然后,回收者视图将显示类别名称,后跟相关产品。
产品存储库实现
fun getProducts(networkId: String) = flow {
request.getAll("network/${networkId}/products", null)
.flowOn(Dispatchers.IO)
.onStart { emit(ApiResult.Loading) }
.catch { emit(ApiResult.Error(it.message.toString())) }
.map { ApiResult.Success(it) }
.also { emitAll(it) }
}
这是加载ServiceDto
的实现private fun loadServices() {
lifecycleScope.launch {
async {
viewModel.getServices(baseViewModel.profile?.networkId ?: "")
}.await().onEach { result ->
when (result) {
is ApiResult.Loading -> {} // Handle loading
is ApiResult.Error -> {} // Handle error
is ApiResult.Success -> createServiceChips(result.data) // Create UI elements
}
}.collect()
}
}
这是加载产品和类别的实现
private fun loadCategoriesAndProducts() {
lifecycleScope.launch {
val products = async {
viewModel.getProducts(networkId).shareIn(lifecycleScope, SharingStarted.WhileSubscribed(), 1 // currently no idea what this means ) // Convert to hotflow to start loading data
}.await()
async { viewModel.getCategories(networkId) }.await().onEach { categories->
when (categories) {
is ApiResult.Loading -> {} // Handle loading
is ApiResult.Error -> {} // Handle error
is ApiResult.Success -> {
publishCategoryUI(categories.data)
collectProducts(products, categories.data)
}
}
}.collect()
}
}
这是我收集产品并将类别和产品映射到平面列表的方法。
private suspend fun collectProducts(products: SharedFlow<ApiResult<List<ProductDto>>>, categories: List<CategoryDto>?) = coroutineScope {
products.onEach{ productResult ->
when (productResult) {
is ApiResult.Success -> {
val productCategoryList = mutableListOf<Any>()
withContext(Dispatchers.IO) {
categories?.forEach { category ->
productCategoryList.add(category.name)
productResult.data?.filter { product ->
product.categoryId == category.id
}.let {
productCategoryList.addAll(it?.toList() ?: emptyList())
}
}
}
productsAdapter.updateData(productCategoryList) {
loadingIndicator.visibility = View.INVISIBLE
}
}
is ApiResult.Error -> // Handle error
is ApiResult.Loading -> {} // Handle loading
}
}.collect()
}
一切工作正常,但我可以看到,当产品被添加到回收视图,它阻塞UI很短的时间。产品显示前加载指示灯滞后。(仅13种产品)
我如何改进实现,或者它是否正确?Kotlin协程和流提供了如此多的可能性,以至于有时很难找到一个好的/正确的解决方案。
你正在做的不正确的事情是无害的,但只会使你的代码变得不必要的复杂:
- 在您的
getProducts
函数中,您不需要将您的流程包装在flow
构建器中,您可以从中emitAll
。删除外部的flow { }
包装和.also { emitAll(it) }
。 - 永远不要做
async { }.await()
模式。这与直接调用lambda内部的代码没有什么不同。当编译器检测到你这样做时,它应该向你发出警告。 onEach { }.collect()
模式可以缩短为.collect { }
,或者根据我的喜好,collect()
可以成为launchIn(scope)
并替换外部的scope.launch
以减少代码嵌套/缩进。- 在
collectProducts()
中,您使用coroutineScope
创建了一个作用域,但从未使用它来运行任何子协程,因此它是毫无意义的。 - 如果你正在使用来自知名库的流,如Retrofit, Room或Firebase,你不需要使用
flowOn
,因为它们正确封装了任何阻塞工作。公共流量不应阻塞其下游收集器。同样,您不应该需要withContext
来从这些库调用挂起函数,因为按照惯例,挂起函数不应该阻塞。
你正在做的不正确和有害的事情:
- 在
loadCategoriesAndProducts()
中,您将SharedFlow传递给另一个收集它的函数。这创造了两个流的乘法,当物品到达时,这两个流将呈指数级增长。这个问题可以更简单地解决,如下所示。你需要做的只是将这两个流组合起来。 - 你应该使用
repeatOnLifecycle
或flowWithLifecycle
来避免收集,而你的活动是在屏幕外,因为这是浪费资源。 - 在你的
categories?.forEach {
块你做嵌套迭代不必要的。
如果你想使用SharedFlow,这可以避免导致新的请求被重做,那么你应该在你的ViewModel中放置一个函数,该函数将networkID
提供给一个MutableShateFlow,该MutableShateFlow作为另一个SharedFlow的基础。你可以在这里私下声明两个并行流,然后公开地组合它们。
// In ViewModel class:
private val _networkId = MutableStateFlow<String?>(null)
var networkId: String?
get() = _networkId.value
set(value) { _networkId.value = value }
private val products = _networkId.filterNotNull().distinctUntilChanged()
.flatMapLatest { networkId ->
request.getAll("network/${networkId}/products", null)
.onStart { emit(ApiResult.Loading) }
.catch { emit(ApiResult.Error(it.message.toString())) }
.map { ApiResult.Success(it) }
}
private val categories = _productsNetworkId.filterNotNull().distinctUntilChanged()
.flatMapLatest { networkId ->
request.getAll("network/${networkId}/categories", null) // I'm just guessing
.onStart { emit(ApiResult.Loading) }
.catch { emit(ApiResult.Error(it.message.toString())) }
.map { ApiResult.Success(it) }
}
val categoriesAndProducts: SharedFlow<ApiResult<List<Any>>> =
products.combine(categories) { p, c ->
when {
p is ApiResult.Loading, c is ApiResult.Loading -> ApiResult.Loading
p is ApiResult.Error -> p
c is ApiResult.Error -> c
else -> { // both are Success
ApiResult.Success(
c.data.orEmpty().flatMap { category ->
listOf(category) + p.data.orEmpty().filter { it.categoryId = category.id }
}
)
}
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), replay = 1)
然后在Activity中:
private fun loadCategoriesAndProducts() {
viewModel.networkId = networkId
viewModel.categoriesAndProducts
.onEach {
when(it) {
is ApiResult.Loading -> {} // Handle loading
is ApiResult.Error -> {} // Handle error
is ApiResult.Success -> {
productsAdapter.updateData(productCategoryList) {
loadingIndicator.visibility = View.INVISIBLE
}
}
}
}
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.launchIn(lifecycleScope)
}
除此之外,使用MutableList<Any>
是一种代码气味,会产生代码可维护性问题。