从API刷新时,RecyclerView无法滚动(OkHttp),使用IndexOutOfBoundsException崩溃

时间:2017-07-05 20:10:39

标签: java android android-recyclerview indexoutofboundsexception recycler-adapter

我的RecyclerView在尝试刷新数据后滚动时会发生IndexOutOfBoundsException崩溃。

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

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

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

以下是其他人建议的一些解决方案:

  1. 使用adapter.notifyDataSetChanged(),但我明白这一点 被认为是Android文档中的“最后手段”

  2. 在adapter.notifyDataSetChanged()

  3. 之前调用list.clear
  4. 使用adapter.getItemCount()将数据集中所有当前项的位置设置为名为“position”的整数,然后将其传递给adapter.notifyItemRangeChanged(position)

  5. 设置adapter.setHasStableIds(true)

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

  7. 显然,如果RecyclerView在LinearLayout中,'notify'方法不起作用(这可能与Android中的旧bug有关,现在可能已修复,但我不确定。)

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

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

    • JobsAdapter(适配器)
    • JobsListItem(Getters and Setters)
    • JobsOut(片段)
    • jobs_recyclerview
    • jobs_listitem

    我只包含适配器和片段的代码,因为我确信布局文件和Getters和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)
    

    如果你有时间,我会非常感谢你的帮助。

    谢谢!

2 个答案:

答案 0 :(得分:2)

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

由于许多因素,这大部分时间都是这样:

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

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

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

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

    如果您使用RXJava使其成为流,则获得积分: - )

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

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

答案 1 :(得分:-1)

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

recylcerview.stopScroll();