Vasant Raval
Vasant Raval

Reputation: 303

How to implement pagination with kotlin

Hello there recently I switch my project from java to kotlin and I'm a very beginner in kotlin, and I want to implement pagination in my recycler view

the recycler view contain images that are stored in firebase, what I want

Option 1 is that first when the fragment is created it loads 20 images and when the user reaches the last image (20) it loads the next 20 images in the recycler view

Option 2

(optional but wanted this kind of pagination in my app )

have you seen Pinterest and Instagram, if you just scroll normally you probably don't see the images are loading until if you scroll very fast, and then it shows the progress bar?

for eg: Pinterest

1: scroll normally or slow Link

2: scroll very fast Link

I want this kind of functionality if possible

what I think is going on in Instagram or Pinterest is for eg

They load probably 20 images at the start and when the user reaches the 10th image (still 10 images are remaining to be seen by the user) they just load the next 10 images so it just maintains the adapter with 20 images instead of reaching the end of the list to load the next images ( hence this would be an exception if user scrolls very fast)


Note: I just don't know what it is said pagination or something else and also not sure that what I said up are the technique used I'm just speculating

Code

Home_Fragment.kt

I only included the required code but if you want more references please tell me I will update the question btw the implemented pagination (don't know is it pagination or something else but it's not working)

val staggeredGridLayoutManager =
            StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)
        postRecyclerView.layoutManager = staggeredGridLayoutManager
        postRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                visibleItemCount = staggeredGridLayoutManager.childCount
                totalItemCount = staggeredGridLayoutManager.itemCount
                val firstVisibleItems: IntArray? = null
                staggeredGridLayoutManager.findFirstVisibleItemPositions(firstVisibleItems)
                if (loading) {
                    if (totalItemCount > previousTotal) {
                        loading = false
                        previousTotal = totalItemCount
                    }
                }
                if (!loading && totalItemCount - visibleItemCount
                    <= firstVisibleItem + visibleThreshold
                ) {
                    data
                    loading = true
                }
            }
        })
        data
        //        setupFirebaseAuth();
        shimmerFrameLayout!!.startShimmer()
        mUploads = ArrayList()
        postsAdapter = PostAdapter_Home(requireContext(), mUploads)
        postRecyclerView.adapter = postsAdapter
        postRecyclerView.scrollToPosition(saved_position)
        //        postsAdapter.stateRestorationPolicy = PREVENT_WHEN_EMPTY;
        return view
    }
 private val data: Unit
        get() {
            databaseReference.addValueEventListener(object : ValueEventListener {
                @SuppressLint("NotifyDataSetChanged")
                override fun onDataChange(snapshot: DataSnapshot) {
                    if (snapshot.exists()) {
                        shimmerFrameLayout!!.stopShimmer()
                        shimmerFrameLayout!!.visibility = View.GONE
                        postRecyclerView.visibility = View.VISIBLE
                        mUploads!!.clear()
                        for (dataSnapshot in snapshot.children) {
                            val upload = dataSnapshot.getValue(Upload::class.java)!!
                            upload.setmKey(dataSnapshot.key)
                            mUploads!!.add(upload)
                        }
                    }
                    postsAdapter!!.setUploads(mUploads)
                    //notify the adapter
                    postsAdapter!!.notifyItemRangeInserted(0, mUploads!!.size)
                    loading = true
                }

                override fun onCancelled(error: DatabaseError) {
                    loading = true
                }
            })
        }

PostAdapter_Home.kt

class PostAdapter_Home(var mcontext: Context, var mUploads: MutableList<Upload?>?) :
    RecyclerView.Adapter<PostAdapter_Home.PostViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
        val view: View
        view = LayoutInflater.from(mcontext)
            .inflate(R.layout.post_item_container_home_ex, parent, false)
        return PostViewHolder(view)
    }

    override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
        val shimmer = ColorHighlightBuilder()
            .setBaseColor(Color.parseColor("#F3F3F3"))
            .setBaseAlpha(1f)
            .setHighlightColor(Color.parseColor("#E7E7E7"))
            .setHighlightAlpha(1f)
            .setDropoff(50f)
            .build()
        val shimmerDrawable = ShimmerDrawable()
        shimmerDrawable.setShimmer(shimmer)
        val uploadCurrent = mUploads?.get(position)
        Glide.with(mcontext)
            .load(uploadCurrent?.getmImageUrl())
            .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
            .placeholder(shimmerDrawable)
            .fitCenter()
            .into(holder.imageView)

    }

    
    override fun getItemCount(): Int {
        return mUploads?.size!!
    }

    //    public long getId() {
    //        return this.id;
    //    }
    //
    //    @Override
    //    public long getItemId(int position) {
    //        return mUploads.get(position).Id;
    //    }
    fun setUploads(uploads: MutableList<Upload?>?) {
        mUploads = uploads
    }

    class PostViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView: ShapeableImageView

        init {
            imageView = itemView.findViewById(R.id.imagePostHome)
        }
    }
}

Upload.kt

package com.example.myappnotfinal.AdaptersAndMore

import com.google.firebase.database.Exclude

class Upload {
    private var mImageUrl: String? = null
    private var mKey: String? = null

    constructor() {}
    constructor(imageUrl: String?) {
        mImageUrl = imageUrl
    }

    fun getmImageUrl(): String? {
        return mImageUrl
    }

    fun setmImageUrl(mImageUrl: String?) {
        this.mImageUrl = mImageUrl
    }

    @Exclude
    fun getmKey(): String? {
        return mKey
    }

    @Exclude
    fun setmKey(Key: String?) {
        mKey = Key
    }
}

Upvotes: 3

Views: 12344

Answers (1)

Anshul
Anshul

Reputation: 1669

Best will be to use Android's Paging Library with PagingDataAdapter for RV, you can read Pro's and Con's online,

It solves your problem for

  1. loading Paged Data

  2. Since it loads next page before, you wont have to worry about slow and fast scroll

You need to create a PagingSource for loading data

class PagingSource<T: BaseModel> (
    private val endPoint : suspend (Int) -> ApiResult<PageResult<T?>>,
    private val filters : suspend (T) -> Boolean = {true}
) : PagingSource<Int ,T>() {

    companion object{
        const val DEFAULT_STARTING_PAGE_INDEX = 0
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
        Timber.d("load Called for : ${endPoint::class}")
        val pageIndex = params.key ?: DEFAULT_STARTING_PAGE_INDEX
        val responsePageable = endPoint(pageIndex)
        val responseData : MutableList<T> = mutableListOf()
        val nextKey: Int?
        when( responsePageable ) {
            is ApiResult.Success -> {
                if(responsePageable.data.content.isNotEmpty()) {
                    nextKey = pageIndex + 1
                    responseData.addAll(responsePageable.data.content
                        .filterNotNull()
                        .filter { filters(it) }
                    )
                }
                else {
                    nextKey = null
                }
            }
            is ApiResult.NetworkError -> {
                return LoadResult.Error(
                    Throwable("No network connection!")
                )
            }
            else -> {
                nextKey = null
            }
        }

        return LoadResult.Page(
            data = responseData ,
            prevKey = if(pageIndex == DEFAULT_STARTING_PAGE_INDEX) null else pageIndex ,
            nextKey = nextKey
        )
    }

    /**
     * The refresh key is used for subsequent calls to PagingSource.Load after the initial load.
     */
    override fun getRefreshKey(state: PagingState<Int, T>): Int? {
        // We need to get the previous key (or next key if previous is null) of the page
        // that was closest to the most recently accessed index.
        // Anchor position is the most recently accessed index.
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

}

above is my custom impl of paging source ref

It uses ApiResult as a response wrapper , you can replace it with Response (Retrofit Default) or relevent API_RESULT files

Here's the example of how to use above PagingSource

fun getCoreCharacters(
        sortParam: Character.Companion.SortCharacter ,
        filters: suspend (Character) -> Boolean
    ) : Flow<PagingData<Character>>{
        return Pager(
            config = PagingConfig(
                pageSize = DEFAULT_PAGE_SIZE,
                enablePlaceholders = false
            ),
            pagingSourceFactory = {
                PagingSource(endPoint = loadMore@{ x: Int ->
                    characterApi.getCoreCharacters(x ,sortParam.value)
                },
                filters = filters
                )
            }
        ).flow
    }

ref

Upvotes: 2

Related Questions