You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Paging3与Selection Library的RecyclerView间歇性抛出IndexOutOfBoundsException(tryGetViewHolderForPositionByDeadline)

使用Paging3与Selection Library的RecyclerView间歇性抛出IndexOutOfBoundsException(tryGetViewHolderForPositionByDeadline)

我太懂这种间歇性崩溃的糟心了——自己测不出来,用户那边却时不时上报,完全像个藏在暗处的小麻烦。从你贴的堆栈和代码来看,这个问题大概率和Paging3的占位符机制、Selection状态同步,或者PagingSource返回的分页元数据不准确有关,毕竟你在PagingConfig里开了enablePlaceholders = true,这个配置虽然能提升滚动流畅度,但很容易在数据切换(搜索、排序)时和RecyclerView的布局逻辑打起来。

先理清楚可能的根因

  1. 占位符与Selection状态不匹配
    当你切换搜索条件或排序方式时,旧的PagingData被替换,但SelectionTracker可能还攥着旧的item位置/ID引用。新的PagingData的占位符位置和实际数据对不上,RecyclerView布局时就会去请求根本不存在的位置,直接触发越界异常。

  2. PagingSource返回的元数据错误
    看你KanjiPagingSource里的loadFuture方法,返回LoadResult.Page时的itemsBefore、itemsAfter计算可能有问题。比如搜索结果为空时,totalCount是0,但如果pageNumber不是0,offset会是pageNumber*pageSize,这时候itemsBefore就会是一个非零值,RecyclerView会误以为前面有占位符,实际却没有内容,布局时自然越界。

  3. 线程安全问题导致数据不一致
    你用了ListenableFuturePagingSource,如果数据库查询(列表和总数)不是原子操作,中间数据被修改的话,会出现totalCount和实际返回的列表数量不匹配,让Paging3对数据总量的判断出错。

针对性解决方案(按验证成本从低到高)

方案1:先关闭占位符快速验证

既然你开了enablePlaceholders = true,先把它关掉试试,这是这类越界问题的高发区:
在ViewModel的Pager配置里修改:

new PagingConfig(
    /* pageSize */ 100,
    /* prefetchDistance */ 40,
    /* enablePlaceholders */ false, // 这里改成false
    /* initialLoadSize */ 100
)

如果关闭后崩溃消失,基本可以确定是占位符和Selection的冲突问题;如果还出现,再往下排查。

方案2:修复PagingSource的元数据计算

如果必须保留占位符,一定要确保每个LoadResult.Page的元数据绝对准确:
修改KanjiPagingSource里的loadFuture逻辑:

// 替换原来的LoadResult.Page返回代码
// 先处理totalCount为0的情况,直接返回空页
if (totalCount == 0) {
    return new LoadResult.Page<>(Collections.emptyList(), null, null, 0, 0);
}

// 正确计算itemsBefore和itemsAfter
int itemsBefore = offset;
int itemsAfter = totalCount - (offset + kanjiList.size());

// 修正nextKey的判断:只有当前页数据未填满totalCount时才返回下一页key
Integer nextKey = !kanjiList.isEmpty() && (offset + kanjiList.size()) < totalCount ? pageNumber + 1 : null;

return new LoadResult.Page<>(kanjiList, prevKey, nextKey, itemsBefore, itemsAfter);

另外,把查询列表和总数的操作放在同一个数据库事务里,避免中间数据变化导致不一致:
在KanjiRepository里加一个原子查询方法:

@Transaction
public Pair<List<Kanji>, Integer> searchKanjiWithCount(String query, String sortOrder, int pageSize, int offset) {
    List<Kanji> list = searchKanjiPagination(query, sortOrder, pageSize, offset);
    int count = getKanjiAmountSearchPagination(query);
    return new Pair<>(list, count);
}

// 无搜索时的原子方法同理
@Transaction
public Pair<List<Kanji>, Integer> getAllKanjiWithCount(String sortOrder, int pageSize, int offset) {
    List<Kanji> list = getAllKanjiSortedPagination(sortOrder, pageSize, offset);
    int count = getKanjiAmountPagination();
    return new Pair<>(list, count);
}

然后在PagingSource里调用这个原子方法:

Pair<List<Kanji>, Integer> result;
if (query != null && !query.isEmpty()) {
    result = kanjiRepository.searchKanjiWithCount(query, sortOrder, pageSize, offset);
} else {
    result = kanjiRepository.getAllKanjiWithCount(sortOrder, pageSize, offset);
}
List<Kanji> kanjiList = result.first;
int totalCount = result.second;

方案3:同步SelectionTracker与PagingData的更新

当切换搜索/排序参数时,旧的Selection状态会绑定在旧的PagingData上,新数据到来时要清空旧选择:
在KanjiListAdapter里加清空选择的方法:

public void clearSelection() {
    if (selectionTracker != null) {
        selectionTracker.clearSelection();
    }
}

然后在Fragment/Activity中观察kanjiPagingData时,每次新数据到来先清空选择:

viewModel.kanjiPagingData.observe(getViewLifecycleOwner(), pagingData -> {
    adapter.clearSelection(); // 先清空旧的选择状态
    adapter.submitData(getLifecycle(), pagingData);
});

另外,SelectionTracker要基于item的唯一ID(比如数据库主键)而不是位置,因为PagingData的位置会随分页变化。确保Adapter正确实现getItemId():

@Override
public long getItemId(int position) {
    Kanji kanji = getItem(position);
    return kanji != null ? kanji.getId() : RecyclerView.NO_ID;
}

创建SelectionTracker时用基于ID的ItemKeyProvider:

SelectionTracker<Long> selectionTracker = new SelectionTracker.Builder<>(
    "kanji-selection",
    recyclerView,
    new ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_CACHED) {
        @Nullable
        @Override
        public Long getKey(int position) {
            Kanji kanji = adapter.getItem(position);
            return kanji != null ? kanji.getId() : null;
        }

        @Override
        public int getPosition(@NonNull Long key) {
            // Paging3中如果用占位符,这里很难准确获取位置,直接返回NO_POSITION即可
            return RecyclerView.NO_POSITION;
        }
    },
    new KanjiDetailsLookup(recyclerView),
    StorageStrategy.createLongStorage()
).build();
adapter.setSelectionTracker(selectionTracker);

最后说两句

这类间歇性问题的核心就是数据状态的一致性——Paging3的分页元数据、RecyclerView的布局预期、Selection的状态三者要完全同步。建议先从关闭占位符开始验证,确定根因后再针对性修复,这样能少走很多弯路。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 08:34:32