androidandroid-architecture-componentsobjectboxandroid-paging-library

Paging integration with Objectbox database in Android


I want use paging3 from jetpack (Android Architecture Components) with Objectbox. But have troubles with loading next pages. When recyclerview scrolled down RemoteMediator doesnt trigger to LoadType.APPEND event. What could be the reasons?

Dependencies:

    implementation "io.objectbox:objectbox-android:2.7.1"
    implementation 'androidx.paging:paging-runtime:3.0.0-alpha06'
    implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
    implementation 'com.squareup.okhttp3:okhttp:4.8.1'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.8.1'
    implementation 'com.squareup.retrofit2:retrofit:2.7.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.7.2'

Paging Source implementation:

class CustomPagingSource(

    private val query: Query<Page>

) : PagingSource<Int, Showcase>() {

    private var observer: DataObserver<List<Page>>? = null
    private var subscription: DataSubscription? = null

    init {

        observer = DataObserver<List<Page>> { invalidate() }.also {

            subscription = query.subscribe().onlyChanges().weak().observer(it)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Showcase> {

        val currentPage = params.key ?: 1

        val prevKey = if (currentPage == 1) null else currentPage - 1
        val nextKey = currentPage + 1

        val pages = when (params) {
            is LoadParams.Refresh -> getPages(0, 1)
            is LoadParams.Prepend -> null
            is LoadParams.Append -> getPages(currentPage - 1, 1)
        }

        val items = pages?.map { it.items }?.flatten() ?: emptyList()

        return LoadResult.Page(
            data = items,
            prevKey = prevKey,
            nextKey = nextKey
        )
    }

    override fun invalidate() {
        super.invalidate()
        subscription?.cancel()
        subscription = null
        observer = null
    }

    private fun getPages(startPosition: Int, count: Int): List<Page> =
        this.query.find(startPosition.toLong(), count.toLong())

    @OptIn(ExperimentalPagingApi::class)
    override fun getRefreshKey(state: PagingState<Int, Showcase>): Int = 1
}

RemoteMediator implementation:

@OptIn(ExperimentalPagingApi::class)
class CustomRemoteMediator(

    private val pullItems: suspend (page: Int, perPage: Int) -> List<Showcase>

) : RemoteMediator<Int, Showcase>() {

    override suspend fun load(loadType: LoadType, state: PagingState<Int, Showcase>): MediatorResult {

        val page = when (loadType) {
            LoadType.REFRESH -> 1
            LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
            LoadType.APPEND -> {
                val nextKey = state.pages.lastOrNull()?.nextKey
                nextKey ?: return MediatorResult.Success(endOfPaginationReached = true)
            }
        }

        val perPage = if (loadType == LoadType.REFRESH) state.config.initialLoadSize else state.config.pageSize

        return try {
            val items = pullItems(page, perPage)
            val endOfPagination = items.size < perPage

            MediatorResult.Success(endOfPaginationReached = endOfPagination)
        } catch (e: Exception) {
            e.printStackTrace()
            MediatorResult.Error(e)
        }
    }
}

Pager creating process:

       @OptIn(ExperimentalCoroutinesApi::class)
    private fun createPagingSource(): CustomPagingSource = CustomPagingSource(query)

    @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
    private val pager by lazy {
        Pager(
            config = PagingConfig(
                pageSize = 5,
                initialLoadSize = 5,
                prefetchDistance = 1
            ),
            remoteMediator = CustomRemoteMediator(::pullShowcases),
            pagingSourceFactory = ::createPagingSource
        ).flow
    }
    
    /**
     * pull items from API and put into database
     */
    private suspend fun pullShowcases(page: Int, perPage: Int): List<Showcase> = withContext(Dispatchers.IO) {

        val showcasesDTO = ApiService.retrofit.getMyShowcases(0.0, 0.0, page, perPage)

        val showcases = showcasesDTO.map {
            Showcase(
                id = it.id,
                title = it.title
            )
        }

        showcaseBox.put(showcases)

        val pageEntity = Page(page.toLong()).also {

            pageBox.attach(it)
            it.items.addAll(showcases)
        }
        pageBox.put(pageEntity)

        return@withContext showcases
    }


Solution

  • PagingSource has an .invalidate() function you can call to trigger Paging to create a new PagingData / PagingSource pair to reflect changes in your Realm DB.

    In your RemoteMediator implementation, you should fetch items from network, then write them to db and then invalidate the PagingSource before returning MediatorResult.Success.

    You should set endOfPaginationReached to true from MediatorResult, if there were no updates to your db and you therefore don't expect to invalidate.

    Btw, Room automatically handles this in its PagingSource implementation, so you may want to look into whether Realm offers some callbacks your PagingSource can listen to or you may need to to keep track yourself.

    EDIT: Root cause for missing remote APPEND call was not setting nextKey to null eventually.