I'm facing an issue in my Kotlin Android app where pagination requests are made repeatedly when using Swipe Refresh. I have shared my codes below, I send page requests to the service, for example, after the first 30 items, I send page 2 requests, but if I swiperefresh, for some reason these page requests are sent one after another and in a very ridiculous way and the recyclerview scrolls to the bottom. I think that since the Recyclerview scrolls to the bottom, page requests are being sent one after the other. I couldn't solve this problem, can you help me?
BroadcasterEventsFragment
@AndroidEntryPoint
class BroadcasterEventsFragment :
BaseFragment<FragmentBroadcasterEventsBinding, BroadcasterEventsViewModel>(
layoutId = R.layout.fragment_broadcaster_events
) { ...
private val eventPagingAdapter: EventPagingAdapter by lazy {
EventPagingAdapter(
ViewType.REGULAR_SMALL_FULL_WIDTH
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupAdapter()
setupNoContentLayout()
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
viewModel.events.collectLatest {
eventPagingAdapter.submitData(it)
binding.swipeRefresh.isRefreshing = false
}
}
viewModel.init(user)
}
private fun setupAdapter() {
binding.recyclerView.apply {
layoutManager = GridLayoutManager(requireContext(), 2)
addItemDecoration(
EventItemDecoration(extraTopMargin = -16)
)
setHasFixedSize(true)
adapter = eventPagingAdapter
}
binding.swipeRefresh.setOnRefreshListener {
eventPagingAdapter.refresh()
}
eventPagingAdapter.clickListener = { event ->
val events = eventPagingAdapter.snapshot().items
val index = events.indexOfFirst { it.id == event.id }
if (index != -1) {
activityViewModel.run {
cacheEvents(events)
when (val activity = requireActivity()) {
is MainActivity -> navigateToFeed(index, page = EventPageProperty.PROFILE)
is FeedActivity -> {
if (feedActivityViewModel.hasPermission) {
navigateToFeed(index, page = EventPageProperty.PROFILE)
} else {
activity.recreate(
index,
pageProperty = EventPageProperty.PROFILE
)
}
}
else -> Unit
}
}
}
}
eventPagingAdapter.onNotifyClicked = { event ->
viewModel.notifyMe(event)
}
}
override fun onViewEvent(viewEvent: ViewEvent) {
when (viewEvent) {
is NotifyMeCompleted -> eventPagingAdapter.notifyDataSetChanged()
is NavigateToLanding -> {
val fragmentNavigation = LandingFragment.fragmentNavigation()
fragmentBackStackManager.showChildFragment(fragmentNavigation)
}
else -> super.onViewEvent(viewEvent)
}
}
BroadcasterEventsViewModel
@HiltViewModel
class BroadcasterEventsViewModel @Inject constructor(
private val prefManager: IPreferenceManager,
private val broadcasterRepository: IBroadcasterRepository,
private val notificationRepository: INotificationRepository,
private val notifyMeUseCase: NotifyMeUseCase,
private val savedStateHandle: SavedStateHandle
) : BaseViewModel() {
lateinit var events: Flow<PagingData<Event>>
fun init(user: User) {
this.user = user
fetchEvents()
}
private fun fetchEvents() {
val fetchStream = when {
isSeller && tabPosition == SELLER_VIDEO_INDEX -> false
isSeller && tabPosition == SELLER_STREAM_INDEX -> true
!isSeller && tabPosition == NO_SELLER_VIDEO_INDEX -> false
!isSeller && tabPosition == NO_SELLER_STREAM_INDEX -> true
else -> false
}
Log.d("page error","fetchEvents count ")
events = Pager(PagingConfig(pageSize = EVENT_DATA_PAGE_SIZE, prefetchDistance = PREFETCH_DISTANCE)) {
BroadcasterEventsPagingSource(
broadcasterRepository = broadcasterRepository,
user = user,
fetchStream = fetchStream
)
}.flow.cachedIn(viewModelScope)
.combine(notifyMeUseCase.notifiedEventIdList) { pagedEvents, notifiedEventIdList ->
pagedEvents.map { it.copy(isNotified = notifiedEventIdList?.contains(it.id)) }
}
}
fun notifyMe(event: Event) {
if (prefManager.token == null) {
sendViewEvent(NavigateToLanding)
return
}
launch(
onError = {
when (it.isAuthException()) {
true -> sendViewEvent(LiveEventsViewEvent.NavigateToLanding)
false -> handleException(it)
}
}
) {
when (notifyMeUseCase.isEventNotified(event.id)) {
true -> {
notificationRepository.cancelNotification(event.id, EVENT.value)
notifyMeUseCase.removeNotifiedEventId(event.id)
}
false -> {
notificationRepository.saveNotification(event.id, EVENT.value)
notifyMeUseCase.addNotifiedEventId(event.id)
}
}
sendViewEvent(NotifyMeCompleted)
}
}
companion object {
const val EVENT_DATA_PAGE_SIZE: Int = 30
const val PREFETCH_DISTANCE = 1
}
}
EventPagingAdapter
class EventPagingAdapter(
private val eventViewType: ViewType
) : PagingDataAdapter<Event, RecyclerView.ViewHolder>(EventDiffCallback) {
var clickListener: ((Event) -> Unit)? = null
var onNotifyClicked: ((Event) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (eventViewType) {
ViewType.REGULAR_SMALL -> SmallRegularViewHolder(
ViewEventSmallBinding.inflate(inflater, parent, false),
onNotifyClicked
)
ViewType.REGULAR_SMALL_FULL_WIDTH -> SmallRegularFullWidthViewHolder(
ViewEventSmallFullWidthBinding.inflate(inflater, parent, false),
onNotifyClicked
)
ViewType.REGULAR_LARGE -> LargeRegularViewHolder(
ViewEventLargeBinding.inflate(inflater, parent, false),
onNotifyClicked
)
ViewType.LIVE_SMALL -> SmallLiveViewHolder(
ViewEventLiveSmallBinding.inflate(inflater, parent, false),
onNotifyClicked
)
ViewType.LIVE_LARGE -> LargeLiveViewHolder(
ViewEventLiveLargeBinding.inflate(inflater, parent, false),
onNotifyClicked
)
ViewType.PRODUCT_DETAIL -> ProductDetailViewHolder(
ViewEventProductDetailBinding.inflate(inflater, parent, false),
onNotifyClicked
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position) ?: return
when (holder) {
is SmallRegularViewHolder -> {
holder.bind(item)
}
is SmallRegularFullWidthViewHolder -> {
holder.bind(item)
}
is SmallLiveViewHolder -> {
holder.bind(item)
}
is LargeRegularViewHolder -> {
holder.bind(item)
}
is LargeLiveViewHolder -> {
holder.bind(item)
}
is ProductDetailViewHolder -> {
holder.bind(item)
}
}
holder.itemView.setOnClickListener {
clickListener?.invoke(item)
}
}
}
BroadcasterEventsPagingSource
class BroadcasterEventsPagingSource(
private val broadcasterRepository: IBroadcasterRepository,
private val user: User,
private val fetchStream : Boolean
) : PagingSource<Int, Event>() {
override fun getRefreshKey(
state: PagingState<Int, Event>
) = state.anchorPosition
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Event> {
return try {
var page = params.key ?: 1
Log.d("page error","page: "+page)
if (page == 0)
page = 1
val result = if(fetchStream) {
broadcasterRepository.getBroadcasterEvents(
user.id,
page
)
}else {
broadcasterRepository.getBroadcasterVideos(
user.id,
page
)
}
LoadResult.Page(
data = result.data,
prevKey = if (page == 1) null else page - 1,
nextKey = if (result.totalCount > page * result.size) {
page + 1
} else {
null
}
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
}
fragment_broadcaster_events.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.clickme.clickmelive.features.broadcaster.events.BroadcasterEventsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/margin_16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:overScrollMode="never"
android:scrollbars="none"
tools:itemCount="3"
tools:listitem="@layout/view_event_small" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.clickme.clickmelive.widget.NoContentLayout
android:id="@+id/layout_no_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
dependencies:
const val RECYCLER_VIEW = "androidx.recyclerview:recyclerview:1.2.1"
const val PAGING = "androidx.paging:paging-runtime:3.0.0"
const val SWIPE_REFRESH = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"