0

I am currently building a Flickr-like app and I do have a question about pagination.

I am currently calling a FlickrApi to retrieve the recent photos. It's working well but I only get the first 100 because the api only returned list of photos using page so it avoid to have 10000 photos in one call.

The JSON returned look like this:

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

My repository to call the api look like this:

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

and the interface is :

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

the viewModel is done like below:


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

What I am trying to achieve to add a loop and place successive call to get page 2, 3....

I cannot figure-out a good way to plug the loop. My goal is really to place a call, update UI through state flow, place a call to next page, add to UI..

I would prefer to do it in the repository rather than the viewModel, but I am not sure what is the best place.

Any idea ?

Seb
  • 2,929
  • 4
  • 30
  • 73
  • Do you want to load every page proactively? How does the API indicate when the last page is reached? – Tenfour04 Jul 27 '23 at 19:41
  • @Tenfour04 The Api send the total number of page. So I can compare current page vs total page. – Seb Jul 27 '23 at 20:13
  • So I know when to stop but I am not sure how to properly call without having a crazy loop – Seb Jul 27 '23 at 20:14
  • What is `getRecentPhotos()` in the repository overriding? You said you want to update the UI even as it is still fetching new pages, so a suspend function isn't appropriate--it should be a flow. But if you have to override something, you're kind of stuck... – Tenfour04 Jul 27 '23 at 20:58

1 Answers1

0

Preface: withContext(Dispatchers.IO) is unnecessary for calling suspend functions. It is only needed for wrapping blocking function calls. I'm assuming your implementation of the FlickrApiHelper suspend function is defined properly so it doesn't block--suspend functions should never block.

You described wanting to update the UI as the pages continue to be retrieved, so what you need to expose is a Flow, not a suspend function. Assuming you can change the definition of whatever interface/abstract function you are overriding in your repository, you can change it to a Flow that looks like this:

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.

I'm using a List that concatenates your FlickrResults from each call. This results in a List of objects containing Lists, which is convoluted to deal with in the ViewModel, so you might want to come up with a different class to represent all your pages put together and use that type instead.

Then in your ViewModel you would probably want to expose a SharedFlow. We can base it off of a private SharedFlow<Unit> to enable the ability to refresh on demand.

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)
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154