PagedListAdapter jumps to beginning of the list on receiving new PagedList
B

3

19

I'm using Paging Library to load data from network using ItemKeyedDataSource. After fetching items user can edit them, this updates are done inside in Memory cache (no database like Room is used).

Now since the PagedList itself cannot be updated (discussed here) I have to recreate PagedList and pass it to the PagedListAdapter.

The update itself is no problem but after updating the recyclerView with the new PagedList, the list jumps to the beginning of the list destroying previous scroll position. Is there anyway to update PagedList while keeping scroll position (like how it works with Room)?

DataSource is implemented this way:

public class MentionKeyedDataSource extends ItemKeyedDataSource<Long, Mention> {

    private Repository repository;
    ...
    private List<Mention> cachedItems;

    public MentionKeyedDataSource(Repository repository, ..., List<Mention> cachedItems){
        super();

        this.repository = repository;
        this.teamId = teamId;
        this.inboxId = inboxId;
        this.filter = filter;
        this.cachedItems = new ArrayList<>(cachedItems);
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Long> params, final @NonNull ItemKeyedDataSource.LoadInitialCallback<Mention> callback) {
        Observable.just(cachedItems)
                .filter(() -> return cachedItems != null && !cachedItems.isEmpty())
                .switchIfEmpty(repository.getItems(..., params.requestedLoadSize).map(...))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(response -> callback.onResult(response.data.list));
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Long> params, final @NonNull ItemKeyedDataSource.LoadCallback<Mention> callback) {
        repository.getOlderItems(..., params.key, params.requestedLoadSize)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(response -> callback.onResult(response.data.list));
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Long> params, final @NonNull ItemKeyedDataSource.LoadCallback<Mention> callback) {
        repository.getNewerItems(..., params.key, params.requestedLoadSize)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(response -> callback.onResult(response.data.list));
    }

    @NonNull
    @Override
    public Long getKey(@NonNull Mention item) {
        return item.id;
    }
}

The PagedList created like this:

PagedList.Config config = new PagedList.Config.Builder()
        .setPageSize(PAGE_SIZE)
        .setInitialLoadSizeHint(preFetchedItems != null && !preFetchedItems.isEmpty()
                ? preFetchedItems.size()
                : PAGE_SIZE * 2
        ).build();

pagedMentionsList = new PagedList.Builder<>(new MentionKeyedDataSource(mRepository, team.id, inbox.id, mCurrentFilter, preFetchedItems)
        , config)
        .setFetchExecutor(ApplicationThreadPool.getBackgroundThreadExecutor())
        .setNotifyExecutor(ApplicationThreadPool.getUIThreadExecutor())
        .build();

The PagedListAdapter is created like this:

public class ItemAdapter extends PagedListAdapter<Item, ItemAdapter.ItemHolder> { //Adapter from google guide, Nothing special here.. }

mAdapter = new ItemAdapter(new DiffUtil.ItemCallback<Mention>() {
            @Override
            public boolean areItemsTheSame(Item oldItem, Item newItem) {
                return oldItem.id == newItem.id;
            }

            @Override
            public boolean areContentsTheSame(Item oldItem, Item newItem) {
                return oldItem.equals(newItem);
            }
        });

, and updated like this:

mAdapter.submitList(pagedList);
Bauske answered 30/6, 2018 at 7:54 Comment(5)
Did you find a fix for it ? I did notice that using .blockingFirst() instead of subscribing, prevents the jumpingAudiometer
Sadly no, since then we switched to ListAdapter using our own pagination.Bauske
I'm having a similar issue, where the recycler jumps to the middle of the list.Flak
@Audiometer Thanks! Thanks a lot, I killed all day looking for a problem! Nowhere in the documentation is it written about it! If not for your answer, I would have died ... Thank you !!!Deglutinate
I am facing the same issue. Did anyone find the solution?Easing
A
10

You should use a blocking call on your observable. If you don't submit the result in the same thread as loadInitial, loadAfter or loadBefore, what happens is that the adapter will compute the diff of the existing list items against an empty list first, and then against the newly loaded items. So effectively it's as if all items were deleted and then inserted again, that is why the list seems to jump to the beginning.

Alibi answered 6/3, 2019 at 22:24 Comment(4)
Couldn't figure out for the life of me why the diff util is not working properly. Thanks @Marc El NaddafFlavouring
@Marc which observable should i have to block? can you elaborate it in detail ?Fabrikoid
I am still facing this issue even though I am using blocking calls. I am using PageKeyedDataSource. Do you guys have insights on what might be happening?Adjoin
@marcnaddaf, thank you so much, you saved my day! I couldn't understand why deleting a single item followed by dataSource.invalidate() call causes the whole RecyclerView state wiping. When I understood the problem (thanks to your answer), I found this article, which helped me, too: jacobstinson.com/posts/the-android-paging-libraryInhuman
M
4

You're not using androidx.paging.ItemKeyedDataSource.LoadInitialParams#requestedInitialKey in your implementation of loadInitial, and I think you should be.

I took a look at another implementation of ItemKeyedDataSource, the one used by autogenerated Room DAO code: LimitOffsetDataSource. Its implementation of loadInitial contains (Apache 2.0 licensed code follows):

// bound the size requested, based on known count final int firstLoadPosition = computeInitialLoadPosition(params, totalCount); final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);

... where those functions do something with params.requestedStartPosition, params.requestedLoadSize and params.pageSize.

So what's going wrong?

Whenever you pass a new PagedList, you need to make sure that it contains the elements that the user is currently scrolled to. Otherwise, your PagedListAdapter will treat this as a removal of these elements. Then, later, when your loadAfter or loadBefore items load those elements, it will treat them as a subsequent insertion of these elements. You need to avoid doing this removal and insertion of any visible items. Since it sounds like you're scrolling to the top, maybe you're accidentally removing all items and inserting them all.

The way I think this works when using Room with PagedLists is:

  1. The database is updated.
  2. A Room observer invalidates the data source.
  3. The PagedListAdapter code spots the invalidation and uses the factory to create a new data source, and calls loadInitial with the params.requestedStartPosition set to a visible element.
  4. A new PagedList is provided to the PagedListAdapter, who runs the diff checking code to see what's actually changed. Usually, nothing has changed to what's visible, but maybe an element has been inserted, changed or removed. Everything outside the initial load is treated as being removed - this shouldn't be noticeable in the UI.
  5. When scrolling, the PagedListAdapter code can spot that new items need to be loaded, and call loadBefore or loadAfter.
  6. When these complete, an entire new PagedList is provided to the PagedListAdapter, who runs the diff checking code to see what's actually changed. Usually - just an insertion.

I'm not sure how that corresponds to what you're trying to do, but maybe that helps? Whenever you provide a new PagedList, it will be diffed against the previous one, and you want to make sure that there's no spurious insertions or deletions, or it can get really confused.

Other ideas

I've also seen issues where PAGE_SIZE is not big enough. The docs recommend several times the maximum number of elements that can be visible at a time.

Myriam answered 19/11, 2018 at 18:20 Comment(1)
"with the params.requestedStartPosition set to a visible element." In my case it is always 0 after invalidationQuinquagesima
H
4

This also happens when DiffUtil.ItemCallback is not correctly implemented. And by correct implementation I mean, you should properly check whether the oldItem and newItem are same or not and accordingly return true or false from areItemsTheSame() and areContentsTheSame() methods.

For example, if I always return false from both of these methods like:

DiffUtil.ItemCallback<Mention>() {
    @Override
    public boolean areItemsTheSame(Item oldItem, Item newItem) {
        return false;
    }

    @Override
    public boolean areContentsTheSame(Item oldItem, Item newItem) {
        return false;
    }
}

The library thinks that all the items are new therefore it jumps to the top to display all the new items.

So make sure you carefully check the oldItem and newItem and properly return true or false based on your comparisons

Horowitz answered 27/5, 2020 at 11:19 Comment(1)
Thank you soo much!! Just 5 hours wasted, but finally fixed! My issue is described by someone else: #65644100Cercus

© 2022 - 2024 — McMap. All rights reserved.