8

Is background threading managed automatically with PagingData as it is with PagedList, and then returned on the main thread?

From the logs below, it appears PagingData is not run on the backgroud thread in the Paging 3 library compared to PagedList in Paging 2's library.

Expect (Based on the Paging Codelab sample)

  • GithubPagingSource override suspend fun load(...) to run on an IO thread.
  • SearchRepositoriesActivity viewModel.searchRepo(query).collectLatest { ... } to run on the main thread.

Observe

  • Both GithubPagingSource override suspend fun load(...) and SearchRepositoriesActivity viewModel.searchRepo(query).collectLatest { ... } run on the main thread.

Paging 2

Threading is handled on the background by PagedList with toLiveData according to the documentation.

If you use LivePagedListBuilder to get a LiveData, it will initialize PagedLists on a background thread for you.

Paging 3

The Paging 3 documentation does not mention how threading is managed. However, from the logs, PagingSource appears to be running the network request on a main thread and returning the PagingData on a main thread.

My Sample Code

I've recreated the Codelab pattern in the CryptoTweets sample app app-simple module.

FeedPagingSource.kt

class FeedPagingSource : PagingSource<Int, Tweet>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Tweet> {
        try {
            val page = params.key ?: 1
            val nextPage = page + 1
            val tweets = Injection.feedService.getTweets(...)
            println("Thread: FeedPagingSource ${Thread.currentThread().name}")
            Log.v(LOG_TAG, "load success: ${tweets}")
            return LoadResult.Page(...)
        } catch (error: Exception) {
            ...
        }
    }
}

FeedRepository.kt

class FeedRepository {
    fun initFeed() = Pager(
        config = PagingConfig(pageSize = FEED_PAGEDLIST_SIZE),
        pagingSourceFactory = { FeedPagingSource() }
    ).flow
}

FeedViewModel.kt

repository.initFeed().onEach {
    println("Thread: FeedViewModel ${Thread.currentThread().name}")
    _feed.value = it
}.launchIn(viewModelScope)

Attempted Solution

In order to run the PagingSource on a background thread, the flow is initiated on Dispatchers.IO. However, the log still shows PagingSource runs on the main thread in FeedPagingSource.kt.

FeedViewModel.kt

repository.initFeed().onEach {
            println("Thread: FeedViewModel ${Thread.currentThread().name}")
            _feed.value = it
}.flowOn(Dispatchers.IO).launchIn(viewModelScope)
AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134
  • Try `.launchIn(viewModelScope + Dispatchers.IO)` – IR42 Sep 21 '20 at 02:12
  • Thanks for the recommendation @IR42. However, this results in the flow data emitting on the IO thread, `DefaultDispatcher-worker-1`, and the `PagingSource` still running on `main` in the code above. – AdamHurwitz Sep 21 '20 at 02:28

2 Answers2

6

It is not running on background thread in Paging 3, even I use lifecycleScope.launch(Dispatchers.IO) in the view model, as the PagingSource is accessed from Main thread when adapter loads. So for Room, I get it working by wrapping the PagingSource database code inside withContext(Dispatchers.IO) {

private const val STARTING_PAGE_INDEX = 0
private const val COUNT_PER_PAGE = 20

class LocalImagesPagingSource() : PagingSource<Int, GalleryImage>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GalleryImage> {
        val page = params.key ?: STARTING_PAGE_INDEX
        return try {
            withContext(Dispatchers.IO) {
                val dao = GalleryImageDatabase.getDatabase(context).galleryImageDao()
                val images = dao.getGalleryImagesByPosition(page * COUNT_PER_PAGE)

                LoadResult.Page(
                    data = images,
                    prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
                    nextKey = if (images.size == 0) null else page + 1
                )
            }
        } catch (exception: IOException) {
            return LoadResult.Error(exception)
        }
    }
}
Alex Bin Zhao
  • 398
  • 3
  • 11
  • I'm having the same problem. Could you please post the sample code which uses "withContext(Dispatchers.IO) {"? – sinek May 26 '21 at 13:05
  • 1
    My original solution is for loading local images from CursorLoader, but also saving and loading additional info from room. I simplify the sample code with just room related code. – Alex Bin Zhao May 26 '21 at 23:39
  • Otherwise just make the Room DAO call a `suspend fun` then you won't have to use `withContext(Dispatchers.IO)` in your `PagingSource`. – Zero Sep 25 '21 at 09:17
1

Kotlin Coroutines

When implementing PagingData and PagingSource with Kotlin Coroutines and making a Retrofit network request, as in the sample code above, Retrofit handles the background threading by default and returns on the main thread.

See StackOverflow: Does Retrofit make network calls on main thread?

RxJava

When using RxJava with Retrofit as the network source, the threading needs to be explicitly specified as shown in the Android documentation sample.

See Android documentation: Page from network and database > Implement a RemoteMediator

AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134