Last active
March 14, 2024 19:29
-
-
Save root-ansh/30ad6a7afd0fa00807622a3fcd6fc0ac to your computer and use it in GitHub Desktop.
Pagination on a nested scroll view!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun NestedScrollView.addPagination(listener: NSVPaginationListener, rv: RecyclerView){ | |
rv.isNestedScrollingEnabled = false | |
this.setOnScrollChangeListener (listener) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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