android Kotlin流程:异步/等待执行阻塞UI

bvuwiixz  于 2023-04-10  发布在  Android
关注(0)|答案(1)|浏览(248)

我想使用Kotlin流从rest endpoint异步加载多个数据,并将其显示在回收器视图中。数据定义为ServiceDtoProductDtoCategoryDto。ProductDto有一个引用CategoryDto的categoryId属性。
每个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()
    }
}

这就是我收集产品并将类别和产品Map到平面列表的方式。

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()
}

一切正常,但我可以看到,当产品被添加到recyclerview,它阻止用户界面很短的时间。加载指示器是滞后之前,产品被显示。(只有13产品)
我如何改进实现,或者它是否正确?Kotlin协程和流提供了如此多的可能性,这使得有时很难找到一个好的/正确的解决方案。

nzk0hqpo

nzk0hqpo1#

你正在做的不正确的事情并没有害处,但只是让你的代码变得不必要的复杂:

  • getProducts函数中,您不需要将流 Package 在emitAllflow构建器中。删除外部flow { } Package 器和.also { emitAll(it) }
  • 永远不要使用async { }.await()模式。这和直接调用lambda中的代码没有什么不同。当编译器检测到你这样做时,它应该会发出警告。
  • onEach { }.collect()模式可以缩短为.collect { },或者按照我的喜好,collect()可以变为launchIn(scope)并替换外部的scope.launch以减少代码嵌套/缩进。
  • collectProducts()中,您使用coroutineScope创建了一个作用域,但从未使用它来运行任何子协程,因此这是毫无意义的。
  • 如果你使用的Flows来自于知名的库,如Retrofit、Room或Firebase,你不需要使用flowOn,因为它们正确地封装了任何阻塞工作。公共流不应该阻塞它们的下游收集器。同样,你不应该需要withContext来调用这些库中的suspend函数,因为按照惯例,suspend函数应该永远不会阻塞。

你正在做的不正确和有害的事情:

  • loadCategoriesAndProducts()中,您将SharedFlow传递给另一个收集它的函数。这是创建两个流的乘法,随着项目的到达,它将呈指数级增长。这可以更简单地解决,如下所示。您需要做的只是将两个流合并。
  • 您应该使用repeatOnLifecycleflowWithLifecycle,以避免在Activity处于屏幕外时收集,因为这会浪费资源。
  • 在你的categories?.forEach {块中,你正在做不必要的嵌套迭代。

如果你想使用SharedFlow,这可以避免导致新的请求必须重新生成,那么你应该在你的ViewModel中放置一个函数,将networkID馈送到一个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)

然后在您的活动中:

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>的使用是一种代码气味,会产生代码可维护性问题。

相关问题