android / architecture-components-samples

Samples for Android Architecture Components.
https://d.android.com/arch
Apache License 2.0
23.4k stars 8.29k forks source link

Paging library crashes when PagingData is changing #857

Closed sereden closed 4 years ago

sereden commented 4 years ago

Hello!

I use library version androidx.paging:paging-runtime:3.0.0-alpha01 I'm trying to implement a multi-page search using pages. The search string could be changed during typing. Also exists some statuses filters which could be changed. As a result, I faced a crash (Collecting from multiple PagingData concurrently is an illegal operation) that it's not possible to replace data while the previous set is processing. And it looks like there is no way to stop processing or receive callback that processing has finished

So basically my Fragment observes data from ViewModel:

lifecycleScope.launch {
            @OptIn(ExperimentalCoroutinesApi::class)
            viewModel.searchData.distinctUntilChanged().collectLatest {
                adapter?.submitData(it)
            }
        }

ViewModel triggers repository:


    @ExperimentalCoroutinesApi
    val searchData = searchRequest.asFlow().flatMapLatest {
        val selectedStatuses = selectedStatuses.value?.toList() ?: emptyList()
        val search = _search.value.orEmpty()
        if (search.isNotBlank() || selectedStatuses.isNotEmpty()) {
            repository.search(search, selectedStatuses.value?.toList() ?: emptyList()) { statuses, total, page ->
                //  some converting data stuff
            }
        } else {
            flow { emit(PagingData.empty<SearchRecyclerModel>()) }
        }
    }

Repository:

    val pagingSource = SearchPagingSource(appDatabase, databaseDataSource, remoteDataSource, dateHelper)
    fun search(search: String, statuses: List<Long>, convertToRecyclerItems: (List<Result>, Int, Int) -> List<SearchRecyclerModel>) =
        Pager(
            PagingConfig(PAGE_SIZE),
            initialKey = SearchData(search, statuses, 0, PAGE_SIZE)
        ) { pagingSource.setConverter(convertToRecyclerItems) }.flow

And SearchPagingSource

class SearchPagingSource(
    private val appDatabase: AppDatabase,
    private val databaseDataSource: DatabaseDataSource,
    private val remoteDataSource: RemoteDataSource,
    private val dateHelper: DateHelper
) :
    PagingSource<SearchData, SearchRecyclerModel>() {
    private val dateFormat = dateHelper.serverResponseDateFormat
    private var convertToRecyclerItems: ((List<Result>, Int, Int) -> List<SearchRecyclerModel>)? = null

    override suspend fun load(params: LoadParams<SearchData>): LoadResult<SearchData, SearchRecyclerModel> {
        return try {
            val key = params.key!!
            val previousKey = if (key.offset == 0) null else key.copy(offset = key.offset - 1)
            var nextKey: SearchData? = null
            var total: Int

            // internet request
            // TODO why is it UI thread?
            val result = remoteDataSource.search(key.criteria, key.statusIds, key.offset, key.limit)

            // Here is response parsing
            // ...
            //

            total = result?.data?.total ?: 0
            nextKey = if (total < key.limit * (key.offset + 1)) null else key.copy(offset = key.offset + 1)

            LoadResult.Page(
                data = convertToRecyclerItems?.invoke(result, total, key.offset) ?: emptyList(),
                prevKey = previousKey,
                nextKey = nextKey
            )
        } catch (e: IOException) {
            LoadResult.Error(e)
        } catch (e: HttpException) {
            LoadResult.Error(e)
        }
    }

    // Convert server response into recycler's items
    fun setConverter(convertToRecyclerItems: (List<Result>, Int, Int) -> List<SearchRecyclerModel>): SearchPagingSource {
        this.convertToRecyclerItems = convertToRecyclerItems
        return this

    }
}

data class SearchData(val criteria: String, val statusIds: List<Long>, val offset: Int, val limit: Int)

Is there a way to handle it somehow?

dlam commented 4 years ago

This sounds like a repro of https://issuetracker.google.com/issues/158048877, which had the underlying issue of suspend submitData not propagating cancellation properly.

This has since been fixed and will be released with alpha02 - would you be able try out the latest SNAPSHOT or when alpha02 releases and report back your result? Instructions for SNAPSHOT here: https://androidx.dev/ (it's just a maven repo).

Btw, I noticed that you forgot to call Flow<PagingData<T>>.cachedIn(viewModelScope) in your ViewModel, which shouldn't be necessary, but it's important to call to prevent recollection from doing a bunch of unnecessary work.

In the future - the best way to report issues with the library is through our bug tracker: https://issuetracker.google.com/issues/new?component=413106&template=1096385

sereden commented 4 years ago

@dlam Thanks for your feedback and help. Yes, I've tried the latest snapshot and it works fine.