PagedListAdapter在接收到新的PagedList时跳到列表的开头

时间:2018-06-30 07:54:52

标签: android-recyclerview android-adapter android-architecture-components android-jetpack

我正在使用分页库通过ItemKeyedDataSource从网络加载数据。提取项目后,用户可以对其进行编辑,此更新将在内存缓存中完成(不使用像Room这样的数据库)。

现在,由于PagedList本身无法更新(讨论了here),我必须重新创建PagedList并将其传递给PagedListAdapter

更新本身没有问题,但是用新的recyclerView更新了PagedList之后,列表跳到列表的开头,破坏了上一个滚动位置。无论如何,是否可以在保持滚动位置的同时更新PagedList(例如它如何与 Room 一起使用)?

数据源是通过以下方式实现的:

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;
    }
}

PagedList 创建如下:

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();

PagedListAdapter 创建如下:

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);
            }
        });

,并像这样更新:

mAdapter.submitList(pagedList);

3 个答案:

答案 0 :(得分:5)

您应该在可观察对象上使用阻止调用。如果您不在与loadInitialloadAfterloadBefore相同的线程中提交结果,则结果是适配器将计算现有列表项的diff首先针对一个空列表,然后针对新加载的项目。如此有效,就好像所有项目都被删除然后再次插入一样,这就是列表似乎跳到开头的原因。

答案 1 :(得分:3)

您在实现androidx.paging.ItemKeyedDataSource.LoadInitialParams#requestedInitialKey时没有使用loadInitial,我认为您应该这样做。

我看了ItemKeyedDataSource的另一种实现,该实现由自动生成的Room DAO代码:LimitOffsetDataSource使用。其loadInitial的实现包含(Apache 2.0 licensed代码如下):

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

...这些功能对params.requestedStartPositionparams.requestedLoadSizeparams.pageSize起作用。

那怎么了?

每当传递新的PagedList时,都需要确保它包含用户当前滚动到的元素。否则,您的PagedListAdapter会将其视为这些元素的删除。然后,稍后,当您的loadAfter或loadBefore项目加载这些元素时,它将把它们视为这些元素的后续插入。您需要避免删除和插入任何可见项。既然听起来像是滚动到顶部,也许是您不小心删除了所有项目并将其全部插入。

我认为将Room与PagedLists一起使用时的工作方式是:

  1. 数据库已更新。
  2. 房间观察者使数据源无效。
  3. PagedListAdapter代码发现无效之处,并使用工厂创建新的数据源,并在loadInitial设置为可见元素的情况下调用params.requestedStartPosition
  4. 新的PagedList提供给PagedListAdapter,后者运行diff检查代码以查看实际更改的内容。通常,什么都没有更改为可见的内容,但是可能已插入,更改或删除了元素。初始负载以外的所有内容都被视为已删除-在用户界面中不应注意到这一点。
  5. 滚动时,PagedListAdapter代码可以发现需要加载新项目,并调用loadBeforeloadAfter
  6. 完成这些操作后,将向PagedListAdapter提供一个完整的新PagedList,后者运行diff检查代码以查看实际更改的内容。通常-只是一个插入。

我不确定这与您要尝试执行的操作相对应,但也许有帮助吗?每当您提供新的PagedList时,它都会与前一个页面相区别,并且您要确保没有虚假的插入或删除,否则可能会造成混淆。

其他想法

我还看到了PAGE_SIZE不够大的问题。文档建议一次可以看到的最大元素数量是原来的几倍。

答案 2 :(得分:2)

如果未正确实现DiffUtil.ItemCallback,也会发生这种情况。通过正确的实现,我的意思是,您应该正确检查oldItemnewItem是否相同,并相应地从true和{返回falseareItemsTheSame() {1}}方法。

例如,如果我总是从这两个方法返回false:

areContentsTheSame()

图书馆认为所有项目都是新的,因此它跳到顶部以显示所有新项目。

因此请确保您仔细检查DiffUtil.ItemCallback<Mention>() { @Override public boolean areItemsTheSame(Item oldItem, Item newItem) { return false; } @Override public boolean areContentsTheSame(Item oldItem, Item newItem) { return false; } } oldItem并根据比较结果正确返回newItemtrue