Skip to content

Instantly share code, notes, and snippets.

@root-ansh
Last active March 14, 2024 19:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save root-ansh/30ad6a7afd0fa00807622a3fcd6fc0ac to your computer and use it in GitHub Desktop.
Save root-ansh/30ad6a7afd0fa00807622a3fcd6fc0ac to your computer and use it in GitHub Desktop.
Pagination on a nested scroll view!
fun NestedScrollView.addPagination(listener: NSVPaginationListener, rv: RecyclerView){
rv.isNestedScrollingEnabled = false
this.setOnScrollChangeListener (listener)
}
import android.view.View
import androidx.core.widget.NestedScrollView
import timber.log.Timber
/**
* when a recycler view can be in nested scroll view, its ability to recycle views is lost as its
* linear layout manager loads all the views at one go. and since all views are loaded at once,
* the layout manager or (recycelr view) don't fire the last child callbacks properly.
*
* For pagination, the basic principle is:
* - X(20) items are loaded in rv, user sees Y(8).
* - while user is scrolling, the recycling logic of RV and LM are firing events which helps
* us determine when the user reaches the last position.
* - when user reaches last position, we fire an api call, which further loads Z(10) more items.
* - now user is at Xth(20) item, and sees Y(8) more it, while X+Z(30) items are loaded in RV
* - user scrolls and cycle continues.
*
* But what to do in case of NSV+RV? add scroll listener to NSV!. so logic becomes
* - X(20) items are loaded in rv, user sees p1+Y(8)+p2,where p1,p2 are more views of
* nested scroll view
* - while user is scrolling, the NSV is now firing events which helps us determine when the
* user reaches the last VIEW of NS.
* - when user reaches last position, we fire an api call, which further loads Z(10) more items.
* - now user is at Xth(20)+p1+p2 item, and sees Y(8) more it, while X+Z(30)+p1+p2 items are loaded
* in NSV
* - user scrolls and cycle continues.
*
* make sure to use this listener on NSV after disabling nested scroll of recycler
*
* */
class NSVPaginationListener(
private val isLastPage: () -> Boolean,
private val isLoading: () -> Boolean,
private val loadMoreItems: () -> Unit
) : NestedScrollView.OnScrollChangeListener {
private val log by lazy {Timber.tag("HLAdp")}
override fun onScrollChange(v: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
val lastChild: View? = v.getChildAt(v.childCount - 1)
val loading = isLoading.invoke()
val lastPage = isLastPage.invoke()
val lastItemHeight = (lastChild?.measuredHeight?:0) - v.measuredHeight
//log.d("onScrollChange() called with: nsv = ${v.hashCode()}, lastChild = ${lastChild?.hashCode()}, loading = $loading, lastPage = $lastPage, lastItemHeight = $lastItemHeight, scrollX = $scrollX, oldScrollX = $oldScrollX, scrollY = $scrollY, oldScrollY = $oldScrollY")
if (lastChild != null
&& (scrollY >= (lastItemHeight))
&& scrollY > oldScrollY
&& !loading
&& !lastPage
) {
//log.d("onScrollChange:calling scroll")
loadMoreItems()
}
}
}
@AndroidEntryPoint
class MyFragment : BaseFragment() {
private lateinit var binding: ViewBinding
private lateinit var rvAdapter: RvAdapter
private val viewModel by viewModels<ViewModel>()
private var request: ApiRequest? = null
private var currentPageNum = 1
private var nextPageNum = 0
private var isLoadingMoreResults = false
private var paginationListener: NSVPaginationListener? = null
private var shouldReplace = false
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.livedata.observe(viewLifecycleOwner, ::onApiResp)
binding.rvList.apply {
adapter = RvAdapter(::onClick).also { rvAdapter = it }
}
NSVPaginationListener(::isLastResult,::isLoadingResults,::onLoadMoreItems).also {
paginationListener = it
binding.nsvList.addPagination(it,binding.rvList)
}
}
private fun onLoadMoreItems() {
shouldReplace = false
makeApiRequest()
}
private fun isLoadingResults(): Boolean {
return isLoadingMoreResults
}
private fun isLastResult(): Boolean {
return nextPageNum==0
}
private fun onApiResp(resp: Resource<ApiResponse>?) {
customProgressBar.toggle(resp?.status==ResourceState.LOADING)
isLoadingMoreResults = resp.status == ResourceState.LOADING
when (resp?.status) {
ResourceState.SUCCESS -> {
customProgressBar.hide()
val newEntries = resp.data?.toList().orEmpty()
nextPageNum = resp.data?.next_page?:0
currentPageNum = resp.data?.current_page_no?:0
if(shouldReplace){
rvAdapter.replace(newEntries)
}else{
rvAdapter.addMore(newEntries)
}
}
else -> {}
}
}
private fun onClick(item: Item) {
}
private fun makeApiRequest() {
request?.let {
it.pageNo = nextPageNum.toString()
viewModel.fetchData(it)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment