Paging 3 Library calls the load method recursively with LoadType.APPEND
Asked Answered
N

1

2

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

Norton answered 12/6, 2021 at 18:40 Comment(3)
What does .mapAsIconSetsEntry() look like? It looks you set endOfPaginationReached based on iconSet, but only insert what comes out of .mapAsIconSetsEntry(). Could you also share your DAO?Peristalsis
.mapAsIconSetsEntry() is a helper function to convert the list of [IconSets] object to a list of [IconSetsEntry] object. Sure, Will add the DAO.Norton
@Peristalsis I have added the DAO. By the way, the API returns ItemKeyed data for which we should use ItemKeyedDataSource implementation with the RemoteMediator for having Network + Local DB model, but official docs only have an explanation for only PageKeyedDataSource. So I need thoughts on a way to implement ItemKeyedDataSource using the RemoteMediator.Norton
R
0

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

Ripplet answered 5/3, 2022 at 8:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.