在协程中正确使用循环来调用 REST API

问题描述 投票:0回答:1

我目前正在构建一个类似 Flickr 的应用程序,我确实有一个关于分页的问题。

我目前正在调用 FlickrApi 来检索最近的照片。它运行良好,但我只得到前 100 张,因为 api 仅使用

page
返回照片列表,因此避免一次调用中有 10000 张照片。

返回的 JSON 如下所示:

{
   "photos": { 
   "page": 1,
   "pages": 10,
   "perpage": 100,
   "total": 1000,
   "photo": [...]

我的调用 api 的存储库如下所示:

    override suspend fun getRecentPhotos(): FlickrResult<FlickrPhotosList?> = withContext(Dispatchers.IO) {
        when (val response = flickrApiHelper.getRecentPhotos()) {
            is FlickrApiResult.OnSuccess -> {
                FlickrResult.OnSuccess(
                    FlickrMapper.fromRetrofitToFlickrPhotosList(response.data)
                )
            }

            is FlickrApiResult.OnError -> {
                FlickrResult.OnError(
                    response.exception
                )
            }
        }
    }

界面是:

interface FlickrApiHelper {
    suspend fun getRecentPhotos(page: Int = 1): FlickrApiResult<RetrofitPhotosItem>
}

viewModel 的完成如下:


fun getRecentPhotos() {
        viewModelScope.launch {
            _flickerPhotoState.value = FlickrState.Loading
            withContext(Dispatchers.IO) {
                when(val result = flickrUseCases.getRecentPhotos()) {
                    is FlickrResult.OnSuccess -> {
                        result.data?.photo?.let {
                            if(it.isNotEmpty()) {
                                _flickerPhotoState.value = FlickrState.DisplayPhotos(
                                    Mapper.fromListFlickrPhotoItemToListPhotoDetails(it)
                                )
                            } else {
                               _flickerPhotoState.value = FlickrState.NoPhotos
                            }
                            return@withContext
                        }
                        _flickerPhotoState.value = FlickrState.NoPhotos
                    }

                    is FlickrResult.OnError -> {
                        _flickerPhotoState.value = FlickrState.Error(result.exception)
                    }
                }
            }
        }
    }

我想要实现的目标是添加一个循环并连续调用以获取第 2、3 页....

我想不出堵塞回路的好方法。我的目标实际上是发出呼叫,通过状态流更新 UI,呼叫下一页,添加到 UI..

我更愿意在存储库中而不是 viewModel 中执行此操作,但我不确定什么是最好的地方。

有什么想法吗?

android kotlin coroutine kotlin-stateflow stateflow
1个回答
0
投票

前言:

withContext(Dispatchers.IO)
对于调用挂起函数是不必要的。仅在包装blocking函数调用时才需要它。我假设您的 FlickrApiHelper 挂起函数的实现已正确定义,因此它不会阻塞 - 挂起函数永远不应该阻塞。

您描述了希望在继续检索页面时更新 UI,因此您需要公开的是 Flow,而不是挂起函数。假设您可以更改存储库中要覆盖的任何接口/抽象函数的定义,您可以将其更改为如下所示的流程:

private val recentPhotosPages: Flow<FlickrResult<FlickrPhotosList?>> = flow {
    var numPages = -1
    var page = 1
    while (numPages < 0 || page <= numPages) {
        when (val response = flickrApiHelper.getRecentPhotos(page)) {
            is FlickrApiResult.OnSuccess -> {
                if (numPages < 0) {
                    numPages = reponse.data?.pages ?: 0 // my guess at how to retrieve page count, adjust accordingly
                }
                FlickrResult.OnSuccess(
                    FlickrMapper.fromRetrofitToFlickrPhotosList(response.data)
                )
            }

            is FlickrApiResult.OnError -> {
                FlickrResult.OnError(
                    response.exception
                )
            }
        }.let { emit(it) }

        if (numPages < 0) {
            return@flow // error on first page retrieval, give up
        }
        page++
    }
}

override val recentPhotosCumulative: Flow<List<FlickrResult<FlickrPhotosList?>>> = recentPhotosPages
    .runningFold<List<FlickrResult<FlickrPhotosList?>>>(emptyList()) { acc, result ->
        acc + result
    }
    .drop(1) // Drop the initial empty emission from runningFold, 
             // otherwise the Loading state in ViewModel's flow would never appear.

我正在使用一个列表来连接每次调用的 FlickrResults。这会产生一个包含列表的对象列表,该列表在 ViewModel 中处理起来很复杂,因此您可能需要提出一个不同的类来表示放在一起的所有页面并使用该类型。

然后在您的 ViewModel 中您可能想要公开一个 SharedFlow。我们可以将其基于私有

SharedFlow<Unit>
,以实现按需刷新的能力。

private val recentPhotosInitiator = MutableSharedFlow<Unit>(replay = 1)
    .also { tryEmit(Unit) }

val flickerPhotoState = recentPhotosInitiator
    .transformLatest {
        emit(FlickrState.Loading)
        flickrUseCases.recentPhotosCumulative.map { listOfFlickrResults ->
            // These incoming results are the concatenated results for you to deal
            // with accordingly. As mentioned above, this is a convoluted nested list unless
            // you create a class to represent the result with concatenated list of pages.

            // This lambda should return a FlickrState.DisplayPhotos or NoPhotos or Error
            // if I understand your original code correctly
        }.let { emitAll(it) }
    }
    .shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), replay = 1)


// Call this if you want to clear the flow and start loading from scratch again
fun refreshPhotoState() {
    recentPhotosInitiator.tryEmit(Unit)
}
© www.soinside.com 2019 - 2024. All rights reserved.