'Paging 3 Library calls the load method recursively with LoadType.APPEND

I am trying to display data from IconFinder API. It seems to be ItemKeyedDataSource for me and I used Paging3 to display the data as it's mentioned in the official docs.

Here is code, please check if there're any issues with the implementation I have done and where is the mistake.

IconSetsRemoteMediator

@OptIn(ExperimentalPagingApi::class)
class IconSetsRemoteMediator(
    private val query: String?,
    private val database: IconsFinderDatabase,
    private val networkService: IconFinderAPIService
) : RemoteMediator<Int, IconSetsEntry>() {

    private val TAG: String? = IconSetsRemoteMediator::class.simpleName
    private val iconSetsDao = database.iconSetsDao
    private val remoteKeysDao = database.remoteKeysDao

    override suspend fun initialize(): InitializeAction {
        // Load fresh data when ever the app is open new
        return InitializeAction.LAUNCH_INITIAL_REFRESH
    }

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

        val iconSetID = when (loadType) {
            LoadType.REFRESH -> {
                
                null
            }
            LoadType.PREPEND -> {

                return MediatorResult.Success(
                    endOfPaginationReached = true
                )
            }
            LoadType.APPEND -> {
                Log.d(TAG, "LoadType.APPEND")

                val lastItem = state.lastItemOrNull()

                if (lastItem == null) {
                    return MediatorResult.Success(
                        endOfPaginationReached = true
                    )
                }
                // Get the last item from the icon-sets list and return its ID
                lastItem.iconset_id
            }
        }

        try {
            // Suspending network load via Retrofit.
            val response = networkService.getAllPublicIconSets(after = iconSetID)
            val iconSets = response.iconsets
            val endOfPaginationReached = iconSets == null || iconSets.isEmpty()


            database.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    // Delete the data in the database
                    iconSetsDao.deleteAllIconSets()
                    //remoteKeysDao.deleteRemoteKeys()
                }

                Log.d(TAG, "iconSets = ${iconSets?.size}")
                Log.d(TAG, "endOfPaginationReached = $endOfPaginationReached")
                Log.d(TAG, "state.anchorPosition = ${state.anchorPosition}")
                Log.d(TAG, "state.pages = ${state.pages.size}")

                val time = System.currentTimeMillis()
                /*val remoteKeys = iconSets!!.map {
                    RemoteKeysEntry(it.iconset_id, time)
                }*/

                // Insert new IconSets data into database, which invalidates the current PagingData,
                // allowing Paging to present the updates in the DB.
                val data = iconSets!!.mapAsIconSetsEntry()
                iconSetsDao.insertAllIconSets(data)

                // Insert the remote key values which set the time at which the data is
                // getting updated in the DB
                //remoteKeysDao.insertRemoteKeys(remoteKeys)
            }
            return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
        } catch (exception: IOException) {
            return MediatorResult.Error(exception)
        } catch (exception: HttpException) {
            return MediatorResult.Error(exception)
        }
    }

}

IconFinderRepository

class IconFinderRepository(
    private val service: IconFinderAPIService,
    private val database: IconsFinderDatabase
) {
    private val TAG: String? = IconFinderRepository::class.simpleName

    fun getPublicIconSets(): Flow<PagingData<IconSetsEntry>> {
        Log.d(TAG, "New Icon Sets query")

        val pagingSourceFactory = { database.iconSetsDao.getIconSets() }

        @OptIn(ExperimentalPagingApi::class)
        return Pager(
            config = PagingConfig(pageSize = NUMBER_OF_ITEMS_TO_FETCH, enablePlaceholders = false),
            remoteMediator = IconSetsRemoteMediator(
                query = null,
                database,
                service
            ),
            pagingSourceFactory = pagingSourceFactory
        ).flow
    }

    companion object {
        const val NUMBER_OF_ITEMS_TO_FETCH = 20
    }
}

IconSetViewHolder

class IconSetViewHolder private constructor(val binding: RecyclerItemIconSetBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(iconSetsEntry: IconSetsEntry?) {
        if (iconSetsEntry == null) {
            //Show the Loading UI
        } else {
            binding.model = iconSetsEntry
            binding.executePendingBindings()
        }
    }

    companion object {
        fun from(parent: ViewGroup): IconSetViewHolder {
            val layoutInflater = LayoutInflater.from(parent.context)
            val binding = RecyclerItemIconSetBinding.inflate(layoutInflater, parent, false)
            return IconSetViewHolder(binding)
        }
    }
}

IconSetAdapter

class IconSetAdapter : PagingDataAdapter<UiModel.IconSetDataItem, ViewHolder>(UI_MODEL_COMPARATOR) {

    companion object {
        private val UI_MODEL_COMPARATOR =
            object : DiffUtil.ItemCallback<UiModel.IconSetDataItem>() {
                override fun areContentsTheSame(
                    oldItem: UiModel.IconSetDataItem,
                    newItem: UiModel.IconSetDataItem
                ): Boolean {
                    return oldItem.iconSetsEntry.iconset_id == newItem.iconSetsEntry.iconset_id
                }

                override fun areItemsTheSame(
                    oldItem: UiModel.IconSetDataItem,
                    newItem: UiModel.IconSetDataItem
                ): Boolean =
                    oldItem == newItem
            }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return if (viewType == R.layout.recycler_item_icon_set) {
            IconSetViewHolder.from(parent)
        } else {
            IconSetViewHolder.from(parent)
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is UiModel.IconSetDataItem -> R.layout.recycler_item_icon_set
            null -> throw UnsupportedOperationException("Unknown view")
            else -> throw UnsupportedOperationException("Unknown view")
        }
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val uiModel = getItem(position)
        uiModel.let {
            when (uiModel) {
                is UiModel.IconSetDataItem -> (holder as IconSetViewHolder).bind(uiModel.iconSetsEntry)
            }
        }
    }
}

HomeFragmentViewModel

class HomeFragmentViewModel(application: Application) : AndroidViewModel(application) {

    private val TAG: String? = HomeFragmentViewModel::class.simpleName
 
    private val repository: IconFinderRepository = IconFinderRepository(
        IconFinderAPIService.create(),
        IconsFinderDatabase.getInstance(application)
    )

    private var iconSetsQueryResult: Flow<PagingData<UiModel.IconSetDataItem>>? = null

    fun iconSetsQuery(): Flow<PagingData<UiModel.IconSetDataItem>> {

        val newResult: Flow<PagingData<UiModel.IconSetDataItem>> = repository.getPublicIconSets()
            .map { pagingData -> pagingData.map { UiModel.IconSetDataItem(it) } }
            .cachedIn(viewModelScope)

        iconSetsQueryResult = newResult
        return newResult

    }

    /**
     * Factory for constructing HomeFragmentViewModel
     */
    class Factory(private val application: Application) : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(HomeFragmentViewModel::class.java)) {
                return HomeFragmentViewModel(application) as T
            }
            throw  IllegalArgumentException("Unable to construct ViewModel")
        }
    }
}

sealed class UiModel {
    data class IconSetDataItem(val iconSetsEntry: IconSetsEntry) : UiModel()
}

IconSetFragment: This is one of the fragments implemented as part of ViewPager. Its parent is a Fragment in an Activity.

class IconSetFragment : Fragment() {

    private val TAG: String = IconSetFragment::class.java.simpleName

    /**
     * Declaring the UI Components
     */
    private lateinit var binding: FragmentIconSetBinding

    private val viewModel: HomeFragmentViewModel by viewModels()
    private val adapter = IconSetAdapter()
    private var job: Job? = null

    companion object {
        fun newInstance(): IconSetFragment {
            return IconSetFragment()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        // Get a reference to the binding object
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_icon_set, container, false)
        Log.d(TAG, "onCreateView")
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initAdapter()

        job?.cancel()
        job = viewLifecycleOwner.lifecycleScope.launch {
            viewModel.iconSetsQuery().collectLatest {
                adapter.submitData(it)
                Log.d(TAG, "collectLatest $it")
            }
        }

    }

    private fun initAdapter() {
        binding.rvIconSetList.adapter = adapter
        /*.withLoadStateHeaderAndFooter(
        header = LoadStateAdapter(), // { adapter.retry() },
        footer = LoadStateAdapter { adapter.retry() }
    )*/
    }
}

IconSetsDao

@Dao
interface IconSetsDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAllIconSets(iconSets: List<IconSetsEntry>)

    @Query("SELECT * FROM icon_sets_table")
    fun getIconSets(): PagingSource<Int, IconSetsEntry>

    @Query("DELETE FROM icon_sets_table")
    suspend fun deleteAllIconSets()
}

This is the Logcat screenshot, the load() method is being invoked without any scrolling action. enter image description here



Solution 1:[1]

I have the similar issue, seems the recursive loading issue is fixed by setting the recyclerView.setHasFixedSize(true)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 eppe