Android Paging-3 — что делать с тем, что все элементы загружаются в один момент?
Вставил код по максимуму, потому что не вижу проблемы - делаю также, как в туториалах, где все работает. Ну приблизительно - там несколько другие адаптеры для 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: 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 } |
{ 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 } |
{ 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); }); |
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) } } } |
// 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) } } }
Дополнительно:
Ответы:
Сам спросил - сам понял...у меня Recycler View был вложен в NestedScrollView
Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.
Пока нет других ответов. Будьте первым, кто поможет автору.
Ответить на вопрос
Для того чтобы решить проблему с тем, что все элементы загружаются в один момент при использовании Android Paging-3, необходимо настроить стратегию загрузки данных.
Android Paging-3 предоставляет различные стратегии загрузки данных, которые позволяют более гибко управлять процессом загрузки элементов. Одной из таких стратегий является использование `PagingConfig` с параметрами `pageSize`, `prefetchDistance` и `enablePlaceholders`.
Например, чтобы загружать элементы постранично, вы можете задать определенный размер страницы с помощью параметра `pageSize`. Это позволит загружать только определенное количество элементов за раз, что уменьшит нагрузку на приложение и повысит производительность.
Также вы можете использовать параметр `prefetchDistance`, который позволяет загружать элементы заранее, до того как они понадобятся. Это поможет избежать ситуации, когда все элементы загружаются в один момент.
Если вы хотите отобразить заглушки вместо элементов, которые еще не загружены, вы можете включить параметр `enablePlaceholders`. Это позволит показывать заглушки вместо элементов, что улучшит пользовательский опыт.
Пример использования `PagingConfig` с заданием параметров:
PagingConfig config = new PagingConfig( pageSize = 10, prefetchDistance = 20, enablePlaceholders = true );
С помощью правильной настройки стратегии загрузки данных в Android Paging-3 вы сможете избежать ситуации, когда все элементы загружаются в один момент, и обеспечить более плавную работу вашего приложения.