cashapp / multiplatform-paging

A library that packages AndroidX Paging for Kotlin/Multiplatform.
Apache License 2.0
506 stars 20 forks source link

`RemoteMediator` implementation not possible? #273

Open JohanAlbrectsen opened 1 month ago

JohanAlbrectsen commented 1 month ago

It seems as if the RemoteMediator class can't be implemented with cash app paging? If this is true, that would be really sad, as it's the most valuable part of the AndroidX paging library that it's trying to mimic.

The issues arise when trying to implement the actual classes:

  1. Type mismatch.
    Required:
    RemoteMediatorMediatorResult
    Found:
    RemoteMediatorMediatorResultSuccess

RemoteMediatorMediatorResult is the expected return type, but can't be initialised. The natural inclination is to use the RemoteMediatorMediatorResultSuccess (which can be initialised) or RemoteMediatorMediatorResultError (which can be initialised), but the method:

override suspend fun load(
        loadType: LoadType,
        state: PagingState<String, FeatureItem>
    ): RemoteMediatorMediatorResult

doesn't accept it.

2. The same issue arises when implementing the Pager. Methods expects: PagingSourceLoadResult:

override suspend fun load(params: PagingSourceLoadParams<String>): PagingSourceLoadResult<String, FeatureItem>

Naturally, using the class PagingSourceLoadResultPage (instead of the original LoadResult.Page from AndroidX Paging, doesn't work for the same reason.

Here's the full codebase of all the classes:

package com.feature.data.remote.mediators

import app.cash.paging.LoadType
import app.cash.paging.Pager
import app.cash.paging.PagingConfig
import app.cash.paging.PagingData
import app.cash.paging.PagingSource
import app.cash.paging.PagingSourceLoadParams
import app.cash.paging.PagingSourceLoadResult
import app.cash.paging.PagingSourceLoadResultPage
import app.cash.paging.PagingState
import app.cash.paging.RemoteMediator
import app.cash.paging.RemoteMediatorMediatorResult
import app.cash.paging.RemoteMediatorMediatorResultSuccess
import app.cash.sqldelight.db.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class FeatureRemoteMediator(
    private val remoteKeysDao: RemoteKeysDao,
    private val featureDao: FeatureDao,
    private val api: FeatureApi
): RemoteMediator<String, FeatureItem>() {

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<String, FeatureItem>
    ): RemoteMediatorMediatorResult {
        val currentPage = when (loadType) {
            LoadType.REFRESH -> {
                val remoteKey = getRemoteKeyClosestToCurrentPosition(state)
                remoteKey?.nextPage
            }
            LoadType.PREPEND -> {
                val remoteKeys = getRemoteKeyForFirstTime(state)
                val prevPage = remoteKeys?.previousPage ?: return RemoteMediatorMediatorResultSuccess(endOfPaginationReached = remoteKeys != null)
                prevPage
            }
            LoadType.APPEND -> {
                val remoteKeys = getRemoteKeyForLastTime(state)
                val nextPage =
                    remoteKeys?.nextPage ?: return RemoteMediatorMediatorResultSuccess(endOfPaginationReached = remoteKeys != null)
                nextPage
            }
            else -> {
                ""
            }
        }

        val response = api.getRemoteItems(
            nextPage = currentPage,
            pageSize = state.config.pageSize
        )

        featureDao.insertItems(items = response.items)

        val remoteKeys = response.items.map { RemoteKeyEntity(id = "rand", objectId = it.id, cacheId = "_cacheid", nextPage = "", previousPage = "", createdAt = 0) }
        remoteKeysDao.insertKeys(list = remoteKeys)

        return RemoteMediatorMediatorResultSuccess(endOfPaginationReached = response.nextKey == null)
    }

    private suspend fun getRemoteKeyClosestToCurrentPosition(
        state: PagingState<String, FeatureItem>,
    ): RemoteKeyEntity? {
        return state.anchorPosition?.let { position ->
            state.closestItemToPosition(position)?.id?.let { objectId ->
                remoteKeysDao.getKeyForItem(itemId = objectId, cacheId = "_cacheId")
            }
        }
    }

    private suspend fun getRemoteKeyForFirstTime(
        state: PagingState<String, FeatureItem>,
    ): RemoteKeyEntity? {
        return state.pages.firstOrNull {
            it.data.isNotEmpty()
        }?.data?.firstOrNull()?.let { featureItem ->
            featureItem.id.let { remoteKeysDao.getKeyForItem(itemId = it, cacheId = "_cacheId") }
        }
    }

    private suspend fun getRemoteKeyForLastTime(
        state: PagingState<String, FeatureItem>,
    ): RemoteKeyEntity? {
        return state.pages.lastOrNull {
            it.data.isNotEmpty()
        }?.data?.lastOrNull()?.let { featureItem ->
            featureItem.id.let { remoteKeysDao.getKeyForItem(itemId = it, cacheId = "_cacheId") }
        }
    }

}

class FeatureRepository(
    private val remoteKeysDao: RemoteKeysDao,
    private val dao: FeatureDao,
    private val api: FeatureApi
) {

    fun getPagingList(): Flow<PagingData<FeatureItem>> {
        val pager = Pager(
            config = PagingConfig(
                pageSize = 12
            ),
            pagingSourceFactory = {
                dao.getPagingSource()
            },
            remoteMediator = FeatureRemoteMediator(
                remoteKeysDao = remoteKeysDao,
                featureDao = dao,
                api = api
            )
        )
        return pager.flow
    }
}

class FeatureApi {

    suspend fun getRemoteItems(nextPage: String?, pageSize: Int): PaginationResponse<FeatureItem> {
        return PaginationResponse(
            items = emptyList(),
            nextKey = null,
            prevKey = null
        )
    }

}

class RemoteKeysDao() {

    suspend fun insertKeys(list: List<RemoteKeyEntity>) {}

    suspend fun getKeyForItem(itemId: String, cacheId: String): RemoteKeyEntity {
        TODO()
    }
}

class FeatureDao {

    fun getPagingSource(): PagingSource<String, FeatureItem> {
        val pagingSource = FeatureItemPagingSource()
        return pagingSource
    }

    suspend fun insertItems(items: List<FeatureItem>) {
        TODO()
    }
}

data class FeatureItem(
    val id: String,
    val hello: String
)

class GetFeatureItemsUseCase(
    private val repository: FeatureRepository
) {

    operator fun invoke(): Flow<PagingData<FeatureItem>> {
        return repository.getPagingList()
    }
}

class FeatureItemPagingSource(): PagingSource<String, FeatureItem>() {

    override suspend fun load(params: PagingSourceLoadParams<String>): PagingSourceLoadResult<String, FeatureItem> {
        val allItems = listOf<FeatureItem>(
            FeatureItem(
                id = "id",
                hello = "Yes!"
            )
        )

        return PagingSourceLoadResultPage(
            data = allItems,
            nextKey = params.key,
            prevKey = null
        )
    }

    override fun getRefreshKey(state: PagingState<String, FeatureItem>): String? {
        TODO()
    }
}

data class RemoteKeyEntity(
    val id: String,
    val objectId: String,
    val cacheId: String,
    val nextPage: String?,
    val previousPage: String?,
    val createdAt: Long = 0
)

data class PaginationResponse<T>(
    val items: List<T>,
    val nextKey: String?,
    val prevKey: String?
)
JohanAlbrectsen commented 1 month ago

@JakeWharton Hi Jake, I understand no one is working on this repo. Does that mean issues aren't getting solved / answered anymore? Is it temporary that no one is working on it anymore? And is there a newer paging library that's being worked on? Would love if someone could guide me on this issue #273. Kind regards

JakeWharton commented 1 month ago

Does that mean issues aren't getting solved / answered anymore?

Yes

Is it temporary that no one is working on it anymore?

I don't know. All I can tell you is the current status.

And is there a newer paging library that's being worked on?

Not by us. We are using this repo as-is.