Вставил код по максимуму, потому что не вижу проблемы - делаю также, как в туториалах, где все работает. Ну приблизительно - там несколько другие адаптеры для Recycler View.
Смотрел на проект с Jetpack Compose (у меня его нет), там data-слой такой же, для presentation есть collectAsLazyPagingItems. Потом смотрел на обычный проект, обновил зависимости, поправил, запустил -
прекрасно работает- там запросы к стороннему api, но можно проверить в logcat по "--> GET", и они реально при прокрутке.
Что у меня - у меня свой тестовый бэкенд на nodejs, вроде бы пагинация должна правильно работать - но нет.
В AppealPagingSource сейчас поставил на мету meta = MetaRequest(page, PAGE_SIZE) = page, limit, т.е. мой PAGE_SIZE, а если ставить
params.loadSize, то
не могу с бэка правильно обработать, - там первый запрос больше, и из-за этого все сбивается, элементы дублируются, если их меньше, чем в 1 запросе.
Но и с PAGE_SIZE в limit, и с params.loadSize
на бэк делаются ВСЕ запросы сразу вне зависимости от величины списка на бэке, PAGE_SIZE и тд. Мне удавалось добиться нормальных запросов на МОЙ БЭК только из другого проекта (не который по ссылке, его сложно переписать и я не стал). Но с проекта по ссылке тоже нормально работает, я в логах вижу.
Т.е. еще раз - хочу например ну 1-2 запроса, а получается какое-то такое чудо, это если элементов в списке на бэке 19.
{ limit: 3, page: 1 }
{ limit: 3, page: 2 }
{ limit: 3, page: 3 }
{ limit: 3, page: 4 }
{ limit: 3, page: 5 }
{ limit: 3, page: 6 }
{ limit: 3, page: 7 }
{ limit: 3, page: 8 }
Аналогично
{ limit: 5, page: 1 }
{ limit: 5, page: 2 }
{ limit: 5, page: 3 }
{ limit: 5, page: 4 }
{ limit: 5, page: 5 }
Это было при PAGE_SIZE, а при params.loadSize такое, но это объяснимо константой в библиотеке
{ limit: 15, page: 1 }
{ limit: 5, page: 2 }
{ limit: 5, page: 3 }
{ limit: 5, page: 4 }
{ limit: 5, page: 5 }
Также пробовал различные комбинации во ViewModel/View - LiveData+MutableLiveData+observe(у меня во всем приложении так), делать collect во ViewModel, делать collectLatest и миллион всего. Признаю, плохо разбираюсь в этом, но что есть - то и имеем.
Как обрабатываю на бэке:
function paginateAppeal(meta) {
console.log(meta);
const { page, limit } = meta;
const startIndex = (page - 1) * limit;
const endIndex = Math.min(startIndex + limit, appealList.length);
const paginatedAppealList = appealList.slice(startIndex, endIndex);
return paginatedAppealList;
}
app.post('/api/event/get-events', (req, res) => {
const eventList = {};
if (req.body.events.includes("Appeal")) {
eventList.appeals = paginateAppeal(req.body.meta);
// console.log(paginateAppeal(req.body.meta))
} else if (req.body.events.includes("Notification") && req.body.events.includes("Voting")) {
eventList.notifications = notificationList;
eventList.votings = votingList;
}
res.json(eventList);
});
Полный код:
// paging
implementation("androidx.paging:paging-common-ktx:3.2.1")
implementation("androidx.paging:paging-runtime-ktx:3.2.1")
interface EventApiService {
@POST("event/get-events")
suspend fun listEvent(
@Body request: EventListRequest
): EventListResponse
@POST("event/update-voting")
suspend fun updateVoting(
@Body request: VotingUpdateRequest
): VoteListItemResponse
companion object {
private const val PAGE_SIZE = 3
}
}
class AppealRepositoryImpl(
private val eventApiService: EventApiService
): AppealRepository {
@OptIn(ExperimentalPagingApi::class)
override fun listAppeal(): Flow<PagingData<AppealListItemResponse>> {
return Pager(
config = PagingConfig(
pageSize = PAGE_SIZE
),
pagingSourceFactory = { AppealPagingSource(eventApiService) }
).flow
}
}
@ExperimentalPagingApi
class AppealPagingSource(private val eventApiService: EventApiService) : PagingSource<Int, AppealListItemResponse>() {
companion object {
const val STARTING_PAGE_INDEX = 1
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AppealListItemResponse> {
val page = params.key ?: STARTING_PAGE_INDEX
return try {
val events = eventApiService.listEvent(
EventListRequest(
userId = 1,
events = listOf(EventType.Appeal),
meta = MetaRequest(page, PAGE_SIZE)
)
)
LoadResult.Page(
data = events.appeals,
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
nextKey = if (events.appeals.isEmpty()) null else page + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, AppealListItemResponse>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
@HiltViewModel
class AppealListViewModel @Inject constructor(
appealRepository: AppealRepository,
val appealMapper: AppealMapper
) : ViewModel() {
private val dataSource = appealRepository.listAppeal()
@OptIn(ExperimentalCoroutinesApi::class)
val appealList: Flow<PagingData<AppealUiModel>> by lazy {
dataSource
.mapLatest { pagingData -> pagingData.map { appealMapper.appealToUi(it) } }
.cachedIn(viewModelScope)
}
}
@AndroidEntryPoint
class AppealListView : Fragment() {
private lateinit var appealListAdapter: CustomPagingAdapter<AppealUiModel, AppealListItemBinding>
override fun onViewCreated(view: View, savedInstanceState: Bundle ?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
viewModel.appealList.collectLatest {
appealListAdapter.submitData(it)
}
}
}
}
open class CustomPagingAdapter<T: Adaptive, B : ViewDataBinding>(
private val itemBindingInflater: (LayoutInflater, ViewGroup, Boolean) -> B,
private val setBinding: (B, T) -> Unit,
private val onItemClick: ((item: T) -> Unit)?
) : PagingDataAdapter<T, CustomPagingAdapter<T, B>.ItemViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = itemBindingInflater(layoutInflater, parent, false)
return ItemViewHolder(binding)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = getItem(position)
item?.run {
holder.bind(this)
holder.itemView.setOnClickListener {
onItemClick?.invoke(this)
}
}
}
inner class ItemViewHolder(val binding: B) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T) {
setBinding(binding, item)
binding.executePendingBindings()
}
}
class DiffCallback<T: Adaptive> : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.areContentsTheSame(newItem)
}
}
}