Kotlin流:async/await执行阻塞UI



我想使用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传递给另一个收集它的函数。这创造了两个流的乘法,当物品到达时,这两个流将呈指数级增长。这个问题可以更简单地解决,如下所示。你需要做的只是将这两个流组合起来。
  • 你应该使用repeatOnLifecycleflowWithLifecycle来避免收集,而你的活动是在屏幕外,因为这是浪费资源。
  • 在你的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>是一种代码气味,会产生代码可维护性问题。

最新更新