使用Paging3与Selection Library的RecyclerView间歇性抛出IndexOutOfBoundsException(tryGetViewHolderForPositionByDeadline)
我太懂这种间歇性崩溃的糟心了——自己测不出来,用户那边却时不时上报,完全像个藏在暗处的小麻烦。从你贴的堆栈和代码来看,这个问题大概率和Paging3的占位符机制、Selection状态同步,或者PagingSource返回的分页元数据不准确有关,毕竟你在PagingConfig里开了enablePlaceholders = true,这个配置虽然能提升滚动流畅度,但很容易在数据切换(搜索、排序)时和RecyclerView的布局逻辑打起来。
先理清楚可能的根因
占位符与Selection状态不匹配
当你切换搜索条件或排序方式时,旧的PagingData被替换,但SelectionTracker可能还攥着旧的item位置/ID引用。新的PagingData的占位符位置和实际数据对不上,RecyclerView布局时就会去请求根本不存在的位置,直接触发越界异常。PagingSource返回的元数据错误
看你KanjiPagingSource里的loadFuture方法,返回LoadResult.Page时的itemsBefore、itemsAfter计算可能有问题。比如搜索结果为空时,totalCount是0,但如果pageNumber不是0,offset会是pageNumber*pageSize,这时候itemsBefore就会是一个非零值,RecyclerView会误以为前面有占位符,实际却没有内容,布局时自然越界。线程安全问题导致数据不一致
你用了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

