【问题标题】:RecyclerView Can't Scroll While Refreshing From API (OkHttp), Crashes With IndexOutOfBoundsException从 API (OkHttp) 刷新时 RecyclerView 无法滚动,出现 IndexOutOfBoundsException 崩溃
【发布时间】:2017-12-09 15:34:12
【问题描述】:

在尝试刷新数据后滚动时,我的 RecyclerView 崩溃并出现 IndexOutOfBoundsException。

所需功能: API 请求成功填充 RecyclerView 一次后,我想刷新 RecyclerView 并能够在刷新时上下滚动。

当前功能:如果我在刷新数据时不滚动,应用程序不会崩溃。如果我在发出刷新请求后滚动,它会因 IndexOutOfBoundsException 而崩溃。

我已经花了几个星期的时间试图解决这个问题而没有发布问题,我相信我已经尝试了足够多的潜在解决方案来证明向 Stack Overflow 寻求指导是合理的。这里有无数关于同一主题的问题,但不幸的是,它们都没有解决我的问题。提前感谢您的考虑。

以下是其他人提出的一些解决方案:

  1. 要使用 adapter.notifyDataSetChanged(),但我理解为 在 Android 文档中被视为“最后的手段”

  2. 在adapter.notifyDataSetChanged()之前调用list.clear

  3. 使用 adapter.getItemCount() 将数据集中所有当前项的位置获取到名为“position”的 Integer,然后将其传递给 adapter.notifyItemRangeChanged(position)

  4. 设置adapter.setHasStableIds(true)

  5. 调用mRecyclerView.getRecycledViewPool().clear()和mAdapter.notifyDataSetChanged();

  6. 显然,如果 RecyclerView 在 LinearLayout 中,“通知”方法不起作用(这可能与 Android 中的一个旧错误有关,现在可能已修复,但我不确定。)

所有这些建议都会导致“致命异常”。

我的应用使用了五个文件:

  • JobsAdapter(适配器)
  • JobsListItem(Getter 和 Setter)
  • JobsOut(片段)
  • jobs_recyclerview
  • jobs_listitem

我只包含了 Adapter 和 Fragment 的代码,因为我确信布局文件以及 Getter 和 Setter 格式正确。

片段:

public class JobsOut extends Fragment {

String jobId;
String jobTitle;
String jobNumber;
String jobStartTime;
String dispatchType;

@BindView(R.id.jobsOutRecyclerView) RecyclerView jobsOutRecyclerView;
@BindView(R.id.fab) FloatingActionButton refreshFab;

private List<JobsListItem> dispatch;
private RecyclerView.Adapter mJobsOutAdapter;
public RecyclerView.LayoutManager dispatchLayoutManager;

OkHttpClient client = new OkHttpClient();
Handler handler = new Handler();

public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View rootView = inflater.inflate(R.layout.recycler_test, container, false);
    ButterKnife.bind(this, rootView);

    dispatch = new ArrayList<>();
    jobsOutRecyclerView.setHasFixedSize(true);
    dispatchLayoutManager = new LinearLayoutManager(getContext());
    jobsOutRecyclerView.setLayoutManager(dispatchLayoutManager);

    downloadDispatch();

    refreshFab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

            downloadDispatch();

            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    dispatch.clear();
                }
            });
        }
    });

    return rootView;
}

@Override
public void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(this);
}

private void downloadDispatch() {
    final okhttp3.Request request = new okhttp3.Request.Builder()
            .url("url")
            .header("X_SUBDOMAIN", "SUBDOMAIN")
            .header("X-AUTH-TOKEN", "API_KEY")
            .build();

    Call call = client.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {

        }

        @Override
        public void onResponse(Call call, okhttp3.Response response) throws IOException {

            try {
                String jsonData = response.body().string();

                JSONObject getRootObject = new JSONObject(jsonData);
                JSONObject metaObject = getRootObject.getJSONObject("meta");
                final String row_count = metaObject.getString("total_row_count");
                {

                    if (row_count.equals("0")) {
                        // do something for no jobs
                    } else {
                        JSONObject getArray = new JSONObject(jsonData);
                        JSONArray opportunitiesArray = getArray.getJSONArray("opportunities");

                        for (int i = 0; i < opportunitiesArray.length(); i++) {
                            JSONObject opportunity = opportunitiesArray.getJSONObject(i);

                            jobId = opportunity.getString("id");
                            jobTitle = opportunity.getString("subject");
                            jobNumber = opportunity.getString("number");
                            jobStartTime = opportunity.getString("starts_at");
                            dispatchType = opportunity.getString("customer_collecting");

                            // Take Strings from response and send them to JobsListItem
                            final JobsListItem item = new JobsListItem(jobId, jobTitle, jobNumber, jobStartTime, dispatchType);

                            // If the adapter hasn't been created, do this
                            if (mJobsOutAdapter == null) {
                                new Handler(Looper.getMainLooper()).post(new Runnable() {
                                    @Override
                                    public void run() {
                                        mJobsOutAdapter = new JobsAdapter(dispatch, getContext());
                                        jobsOutRecyclerView.setAdapter(mJobsOutAdapter);
                                        dispatch.add(item);
                                    }
                                });
                            }
                            // If the adapter has been created, just do this
                            else if (mJobsOutAdapter != null) {
                                new Handler(Looper.getMainLooper()).post(new Runnable() {
                                    @Override
                                    public void run() {
                                        dispatch.add(item);
                                        mJobsOutAdapter.notifyDataSetChanged();
                                    }
                                });
                            }
                        }
                    }
                }
            } catch (IOException e) {
                Log.e("TAG", "IO exception caught: ", e);
            } catch (JSONException e) {
                Log.e("TAG", "TAG exception caught: ", e);
            }
        }
    });
}

适配器:

public class JobsAdapter extends RecyclerView.Adapter<JobsAdapter.ViewHolder> {

private List<JobsListItem> mJobsListItem;
private Context context;

public JobsAdapter(List<JobsListItem> mJobsListItem, Context context) {
    this.mJobsListItem = mJobsListItem;
    this.context = context;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.jobs_listitem, parent, false);

    return new ViewHolder(view);
}

@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
    final JobsListItem mJobsListItemViewHolder = this.mJobsListItem.get(position);

    // holders go here and do things with text and what-not
}

@Override
public int getItemCount() {
    return mJobsListItem.size();
}

public class ViewHolder extends RecyclerView.ViewHolder {

    // BindView's with ButterKnife go here and all that jazz

    public ViewHolder(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);
    }
}

Logcat 崩溃:

26404-26404 E/AndroidRuntime: FATAL EXCEPTION: main
                          Process: uk.co.plasmacat.techmate, PID: 26404
                          java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 4(offset:4).state:16
                              at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5504)
                              at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5440)
                              at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5436)
                              at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2224)
                              at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1551)
                              at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1511)
                              at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1325)
                              at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1061)
                              at android.support.v7.widget.RecyclerView.scrollByInternal(RecyclerView.java:1695)
                              at android.support.v7.widget.RecyclerView.onTouchEvent(RecyclerView.java:2883)
                              at android.view.View.dispatchTouchEvent(View.java:10063)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2630)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2307)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
                              at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
                              at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:413)
                              at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1819)
                              at android.app.Activity.dispatchTouchEvent(Activity.java:3127)
                              at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:71)
                              at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:71)
                              at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:375)
                              at android.view.View.dispatchPointerEvent(View.java:10283)
                                  at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4522)
                              at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4353)
                              at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3893)
                              at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3946)
                              at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3912)
                              at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4039)
                              at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3920)
                              at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4096)
                              at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3893)
                              at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3946)
                              at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3912)
                              at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3920)
                              at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3893)
                              at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6341)
                              at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6315)
                              at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6265)
                              at 

android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6444)
                                  at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
                                  at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
                                  at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:176)
                                  at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:6415)
                                  at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:6467)
                                  at android.view.Choreographer$CallbackRecord.run(Choreographer.java:874)
                                  at android.view.Choreographer.doCallbacks(Choreographer.java:686)
                                  at android.view.Choreographer.doFrame(Choreographer.java:615)
                                  at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:860)
                                  at android.os.Handler.handleCallback(Handler.java:751)
                                  at android.os.Handler.dispatchMessage(Handler.java:95)
                                  at android.os.Looper.loop(Looper.java:154)
                                  at android.app.ActivityThread.main(ActivityThread.java:6290)
                                  at java.lang.reflect.Method.invoke(Native Method)
                                  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
                                  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

如果您有时间,我将非常感谢您的帮助。

谢谢!

【问题讨论】:

  • mRecyclerView.getRecycledViewPool().clear(); mAdapter.notifyDataSetChanged();
  • 嗨@g-singh,我应该说,我也试过了。同样的错误。
  • 如果你愿意放在github上(除非隐私问题)。我很想看看它。Log cat 没有提供足够的信息。
  • 您对notifyDataSetChanged() 的调用次数过多。实际上每个 JSON 条目一次。那是无效的,也许它与崩溃有关。我建议将所有数据项添加到“调度”List,然后在 for-cycle 之后,您应该只调用一次 notifyDataSetChanged()
  • 您是否一起尝试过这些步骤:1- 在您的onResponse 中调用dispatch.clear(); 2- 在您的for loop 之后调用mJobsOutAdapter.notifyDataSetChanged();onResponse

标签: java android indexoutofboundsexception android-recyclerview


【解决方案1】:

您尝试做的事情很常见,当回收器视图需要向其适配器询问数据(因为它已经滚动)并且它需要的位置在 Adatper 中不存在时,您的索引超出范围就会发生。例如:适配器尝试抓取项目编号“N”并且数据包含 N-1(或更少)。

这在大多数情况下是由于多种因素造成的:

  1. 线程。这应该(大部分)在 UI 线程(通知等)上处理。网络请求显然是在后台线程中发生的,我认为最终onResponse 现在回到了主线程(否则你会得到其他异常)。仔细检查我的测试Looper.getMainLooper() == Looper.myLooper()(或类似的)。

  2. 您在主线程上做了很多(不需要的)工作。您收到来自网络的响应,然后解析 JSON 并在主线程中创建对象……为什么不卸载所有工作,一旦您有了项目列表,将其传递给适配器。 p>

  3. 您每次都在低效地调用notifyDataSetChanged()(这很糟糕)。为什么不使用(包含在 Android 中)DiffUtil 类来仅通知更改的范围?请允许我向您指出它是如何工作的一个很好的示例:https://guides.codepath.com/android/using-the-recyclerview#diffing-larger-changes

实施这些更改大约需要 30 分钟,这将使您的代码更加健壮。

如果您使用 RXJava 使其成为流,则可以加分 :-)

注意:您应该创建一次适配器,然后在每次有新数据时调用setItems(your_list_of_items)。 DiffUtil 和适配器应该知道如何处理这个问题。你的活动/片段/网络代码中有很多不属于那里的“业务逻辑”。您所有的“onResponse”方法应该做的是准备数据并将其传递给负责管理数据的类(适配器)。当我看到这个// If the adapter hasn't been created, do this 时,我皱起了眉头。为什么这段代码在这里创建适配器?谁来测试这个?如果你用别的东西改变 OKHttp 怎么办? (为什么不使用改造,让它更容易?)。

我的意思是,你可以做很多事情来让你作为程序员的生活更轻松,但你并没有利用你可用的解决方案。

【讨论】:

  • 嗨,马丁,这是一个绝妙的答案,感谢您抽出宝贵的时间来编写它。我没有测试过你的建议,但我会好好尝试一下,然后返回结果。捍卫我低效的代码;我只做了一个月,所以我的理解仍然是外行,但我喜欢使用图书馆让我的生活更轻松,而且我每天都在学习更多做同样的事情。这个答案无疑将是一个很大的帮助。谢谢!
  • 不客气。抱歉,如果这听起来很刻薄,那不是本意。我试图让你尽快回到正轨。我的建议是查看 Retrofit + GSON(或任何其他 JSON 解析器)。会让你的生活更简单,除非你有“法律”反对包括开源代码(如果你使用 OkHttp,你已经在使用 Square 的库)。特别是如果你是新手,使用 Retrofit(它在内部使用 OKHTTP)将抽象问题,直到你有更多的知识来自己处理它们。然后你变得懒惰并使用Retrofit。 :-)
  • 没问题我的朋友,它很受欢迎。当我第一次学习如何使用 API 调用和 JSON 时,我发现 OkHttp 更易于阅读并且更容易学习基本实现,但随着时间的推移,我偶尔会想到我应该使用 Retrofit 和解析器。这个实现是老狗懒得学习新技巧的例子。
  • 我完全听到了你的声音,但你需要 1 小时才能将它转换为改造/GSON(或任何其他解析器,GSON 工作正常,而且它很简单/集成良好)。你的代码将被简化为“重要部分”。又名:我得到了 的列表,现在我将在 UI 线程中将它传递给我的适配器。就您必须编写的代码而言,其中大部分是“免费”的。
  • 这是正确答案。很棒的建议,马丁。谢谢你的帮助。我使用您链接的 DiffUtil 示例实现了一个测试,它按预期工作。我现在就开始学习 Retrofit 和 GSON,哈!
【解决方案2】:

虽然这里有很多很好的建议。一种想法是在用户与调用额外加载的按钮交互时停止回收器滚动。

recylcerview.stopScroll();

【讨论】:

  • 这不是一个合理或可扩展的解决方案。
  • 如果你这么说,但添加此代码以防止它在边缘情况下崩溃仍然是个好主意。
猜你喜欢
  • 2021-12-31
  • 2017-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-12
  • 1970-01-01
  • 2019-05-25
  • 2017-12-17
相关资源
最近更新 更多