Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android RecyclerView IllegalArgumentException: called detach on an already detached child ViewHolder #4

Closed
guruduttstay opened this issue Sep 27, 2016 · 13 comments

Comments

@guruduttstay
Copy link

guruduttstay commented Sep 27, 2016

I am getting following error sometime (not every time) while updating recycler adapter.

Note : I am using RecyclerViewMergeAdapter

What I am doing here is :

I have created 3 different type adapter and based on need it is trying to merge 9 adapter in a loop and all these 9 adapter can of of one of the 3 types we have.

All adapters are merged to RecyclerViewMergeAdapter but at some point it is crashing

Also Notice : IF all my sub adapter contains 3 or less items then it works fine but if my sub adapters contains more than 3 to 20 items then it crashes.

Please let me know if someone of have seen this issue and found any solution to this.

java.lang.IllegalArgumentException: called detach on an already detached child ViewHolder{7c7ecfb position=2 id=-1, oldPos=-1, pLpos:-1 scrap tmpDetached no parent}
  at android.support.v7.widget.RecyclerView$4.detachViewFromParent(RecyclerView.java:605)
  at android.support.v7.widget.ChildHelper.detachViewFromParent(ChildHelper.java:284)
  at android.support.v7.widget.RecyclerView$LayoutManager.detachViewInternal(RecyclerView.java:6473)
  at android.support.v7.widget.RecyclerView$LayoutManager.detachViewAt(RecyclerView.java:6466)
  at android.support.v7.widget.RecyclerView$LayoutManager.scrapOrRecycleView(RecyclerView.java:6835)
  at android.support.v7.widget.RecyclerView$LayoutManager.detachAndScrapAttachedViews(RecyclerView.java:6818)
  at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:532)
  at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:2847)
  at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:3145)
  at android.view.View.layout(View.java:16636)
  at android.view.ViewGroup.layout(ViewGroup.java:5437)
  at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1079)
  at android.view.View.layout(View.java:16636)
  at android.view.ViewGroup.layout(ViewGroup.java:5437)
  at android.support.v4.view.ViewPager.onLayout(ViewPager.java:1627)
  at android.view.View.layout(View.java:16636)
  at android.view.ViewGroup.layout(ViewGroup.java:5437)
  at android.support.design.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1034)
  at android.support.design.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:744)
  at android.support.design.widget.ViewOffsetBehavior.onLayoutChild(ViewOffsetBehavior.java:42)
  at android.support.design.widget.AppBarLayout$ScrollingViewBehavior.onLayoutChild(AppBarLayout.java:1180)
  at android.support.design.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:757)
  at android.view.View.layout(View.java:16636)
  at android.view.ViewGroup.layout(ViewGroup.java:5437)
  at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336)
  at android.widget.FrameLayout.onLayout(FrameLayout.java:273)
  at android.view.View.layout(View.java:16636)
  at android.view.ViewGroup.layout(ViewGroup.java:5437)
  at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1743)
  at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1586)
  at android.widget.LinearLayout.onLayout(LinearLayout.java:1495)
  at android.view.View.layout(View.java:16636)
  at android.view.ViewGroup.layout(ViewGroup.java:5437)
  at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336)
  at android.widget.FrameLayout.onLayout(FrameLayout.java:273)
  at com.android.internal.policy.PhoneWindow$DecorView.onLayout(PhoneWindow.java:2678)
  at android.view.View.layout(View.java:16636)
  at android.view.ViewGroup.layout(ViewGroup.java:5437)
  at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2171)
  at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1931)
  at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1107)
  at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6013)
  at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858)
  at android.view.Choreographer.doCallbacks(Choreographer.java:670)
  at android.view.Choreographer.doFrame(Choreographer.java:606)
  at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844)
  at android.os.Handler.handleCallback(Handler.java:739)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:148)
  at android.app.ActivityThread.main(ActivityThread.java:5417)
  at java.lang.reflect.Method.invoke(Native Method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

@ronaldwolvers
Copy link
Contributor

Hi, can you please copy and past the code snippet(s) in which you instantiate the adapter, add the sub-adapters to the adapter and where you possibly notify the adapter of data changes? This would be a tremendous help.

@guruduttstay
Copy link
Author

Hi Ronaldw

Thank you for response

I have simplified my code and changed it a bit, changed function and variable names and logic to a bit but code looks like this


    private void updateMyAdapter() {

        ModelA modelA = CManager.getInstance().getModel(mAModelID);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

        if (recyclerView == null || modelA == null) {
            return;
        }

        Activity activity = getActivity();
        RecyclerViewMergeAdapter adapter = new RecyclerViewMergeAdapter();
        recyclerView.setAdapter(adapter);

        if (mImageHeader != null) { 

// NB : mImageHeader is created onCreateView =>  View mImageHeader = inflater.inflate(R.layout.header_image, recyclerView, false);


            adapter.addView(mImageHeader);
        }

        View modelHeading = null;

        List<ModelB> favModelBs = CManager.getInstance().getModelBForModelA(mAModelID);

        if (favModelBs != null && !favModelBs.isEmpty() && activity != null) {
            MyRecyclerAdapter myRecyclerAdapter = new MyRecyclerAdapter(favModelBs, mAModelID, DisplayMode.MODEL_HINT);
            myRecyclerAdapter.setItemClickListener(this);
            modelHeading = inflateListLabel(recyclerView, getResources().getString(R.string.modelHeading), true); // inflateListLabel function inflate layout
            adapter.addView(modelHeading);
            adapter.addAdapter(myRecyclerAdapter);
        }

        List<String> groupingIds = modelA.getGroupingIDs();

        if (groupingIds != null && !groupingIds.isEmpty()) {
            for (String groupingId : groupingIds) {
                GroupingModel grouping = CManager.getInstance().FetchGrouping(mAModelID, groupingId);
                if (grouping != null) {
                    List<ModelB> modelList = grouping.getModelBList();
                    if (modelList != null && !modelList.isEmpty() && activity != null) {
                        MyRecyclerAdapter myRecyclerAdapter = new MyRecyclerAdapter(modelList, mAModelID, DisplayMode.MODEL_HINT);
                        myRecyclerAdapter.setItemClickListener(this);
                        adapter.addView(inflateListLabel(recyclerView, grouping.getName(), updatePadding));
                        adapter.addAdapter(myRecyclerAdapter);
                    }
                }
            }
        }

        adapter.notifyDataSetChanged();
    }

And function call

    private View inflateListLabel(ViewGroup container, String string, boolean updatePadding) {
        LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View inflateView = inflater.inflate(R.layout.grouping_titles, container, false);
        if (inflateView != null) {
            TextView textView = (TextView) inflateView.findViewById(R.id.title_text);
            if (textView != null) {
                textView.setText(Html.fromHtml(string));
                if (updatePadding) {
                    RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                    lp.setMargins(getResources().getDimensionPixelSize(R.dimen.marginTwo), getResources().getDimensionPixelSize(R.dimen.marginTwo), getResources().getDimensionPixelSize(R.dimen.marginTwo), getResources().getDimensionPixelSize(R.dimen.marginFour));
                    textView.setLayoutParams(lp);
                }
            }
        }
        return inflateView;
    }




@ronaldwolvers
Copy link
Contributor

ronaldwolvers commented Sep 29, 2016

Hi, @guruduttstay. The reason I am asking is because I have seen this problem occur before in my own project when I was not using the correct notify<operation>() method or I was calling notifyDataSetChanged() when actually my activity already had its View-s destroyed. Are you calling this method from a Fragment? If so, I would recommend always checking whether you are attached to an Activity by calling isAdded() and making sure you are added before notifying.

Furthermore, I would recommend using notifyItemRangeInserted() instead of notifyDataSetChanged(). For instance when you add an adapter by calling adapter.addAdapter(mRecyclerAdapter) you can call adapter.notifyItemRangeInserted(adapter.getItemCount() - 1 - mRecyclerAdapter.getItemCount(), adapter.getItemCount() - 1). This way you are informing the adapter that you have inserted some new items and using a less heavy operation than the RecyclerView having to recalculate everything.

Meanwhile, we have just merged a Pull Request that has quite drastically overhauled the source code for the adapter. If you'd be willing to try out this code it would be a tremendous help. I have been using the adapter in this way in my own project for a very long time and it works fantastically. Hope you'll fix the crash.

@guruduttstay
Copy link
Author

Hi @ronaldw

Thank you for your help.

If you'd be willing to try out this code it would be a tremendous help.

Sure I will try this

JFYI : I fond this crash more often on appcompat-v7:23.4.0 and heigher version

I have seen this issue only 2 times with appcompat-v7:23.0.1 .... not sure if that is something you want to look into

I will try your suggestion and get back to you asap

let me know how can I try your merged branch

Thank you

@ronaldwolvers
Copy link
Contributor

The changes have been merged to master just now so just pull from this repo and use the new RecyclerViewMergeAdapter. It should be mostly backwards compatible. I personally rarely use the appcompat libraries, so I have no clue as to whether that could be a factor. I am currently just creating a simply activity and adding views to it using addView() and then clearing them using the new call clearAdapters. Are you using the code here or the library on bintray?

@ronaldwolvers
Copy link
Contributor

Just now we have published version 2.0.0 on bintray. Use:

compile 'me.mvdw.recyclerviewmergeadapter:recyclerviewmergeadapter:2.0.0'

If you find any issues with the adapter, please file another issue. I'd be happy to help you solve any bugs if you come across them.

@ronaldwolvers
Copy link
Contributor

Finally, if you are curious about how to properly use the adapter and the notify<operation>() calls, I have just created a very small app that serves to simply test RecyclerViewMergeAdapter's capabilities. Link: https://github.com/ronaldw/RecyclerViewMergeAdapterTest

@guruduttstay
Copy link
Author

I am still getting this issue :(

@guruduttstay
Copy link
Author

Hi

I have updated my code with updated RecyclerViewMergeAdapter (2.x)
and it gives better results but still in some rare cases it crashes with appCompat 23.0.1

but with appCompat 23.4.0 and higher versions still it I get this issue more often

private void updateMyAdapter() {

        ModelA modelA = CManager.getInstance().getModel(mAModelID);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

        if (recyclerView == null || modelA == null) {
            return;
        }

        Activity activity = getActivity();

       if (recyclerView.getAdapter() == null) {
          recyclerView.setAdapter(new RecyclerViewMergeAdapter());
       } else {
           clearAdapters();
       }


        if (mImageHeader != null) { 
            // NB : mImageHeader is created onCreateView =>  View mImageHeader = inflater.inflate(R.layout.header_image, recyclerView, false);
            appendView(mImageHeader);
        }

        View modelHeading = null;

        List<ModelB> favModelBs = CManager.getInstance().getModelBForModelA(mAModelID);

        if (favModelBs != null && !favModelBs.isEmpty() && activity != null) {
            MyRecyclerAdapter myRecyclerAdapter = new MyRecyclerAdapter(favModelBs, mAModelID, DisplayMode.MODEL_HINT);
            myRecyclerAdapter.setItemClickListener(this);
            modelHeading = inflateListLabel(recyclerView, getResources().getString(R.string.modelHeading), true); // inflateListLabel function inflate layout
            if (isAdded()) {
                appendView(modelHeading);
                myRecyclerAdapter.notifyDataSetChanged();
                appendAdapter(myRecyclerAdapter);
            }
        }

        List<String> groupingIds = modelA.getGroupingIDs();

        if (groupingIds != null && !groupingIds.isEmpty()) {
            for (String groupingId : groupingIds) {
                GroupingModel grouping = CManager.getInstance().FetchGrouping(mAModelID, groupingId);
                if (grouping != null) {
                    List<ModelB> modelList = grouping.getModelBList();
                    if (modelList != null && !modelList.isEmpty() && activity != null) {
                        MyRecyclerAdapter myRecyclerAdapter = new MyRecyclerAdapter(modelList, mAModelID, DisplayMode.MODEL_HINT);
                        myRecyclerAdapter.setItemClickListener(this);
                        if (isAdded()) {
                            appendView(inflateListLabel(recyclerView, grouping.getName(), updatePadding));
                            myRecyclerAdapter.notifyDataSetChanged();
                            appendAdapter(myRecyclerAdapter);
                        }
                    }
                }
            }
        }

       RecyclerViewMergeAdapter adapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();
       if (isAdded() && adapter != null) {
           adapter.notifyDataSetChanged();
       }
    }

private void appendAdapter(MyRecyclerAdapter myRecyclerAdapter) {
    
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

    if (recyclerView != null) {
        
        RecyclerViewMergeAdapter adapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();

        if (adapter != null) {

             adapter.addAdapter(myRecyclerAdapter);

        }
    
    }

}




private void appendView(View view) {

    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

    if (recyclerView != null && view != null) {

        RecyclerViewMergeAdapter adapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();

        if (adapter != null) {

            adapter.addView(view);

        }

    }

}



private void clearAdapters() {

    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

    if (recyclerView != null && isAdded()) {

        RecyclerViewMergeAdapter recyclerViewMergeAdapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();

        recyclerViewMergeAdapter.clearAdapters();

        int itemCountToBeCleared = recyclerViewMergeAdapter.getItemCount();

        recyclerViewMergeAdapter.notifyItemRangeRemoved(0, itemCountToBeCleared);

    }

}




@ronaldwolvers
Copy link
Contributor

ronaldwolvers commented Sep 30, 2016

Hi @guruduttstay . Sorry that these problems keep bothering you. The RecyclerView is a rather complex component so it is not easy for me to say what is causing the exception immediately. However, I did notice in your code that you call notifyItemRangeRemoved() with itemCountToBeCleared set to 0. After you call clearAdapters() the adapter will return 0 for getItemCount() hence you would have to call it before you clear it.

Also, when you call adapter.addView(), make sure you call notifyItemRangeInserted(). A single adapter is inserted when this happens, but you always have to call this method for as many items as are there are in your sub adapter. I will look into possible causes of this issue today, but I have had it myself before and it was caused by not calling the correct notify<operation>() methods.

@ronaldwolvers
Copy link
Contributor

Hi again,

To the best of my efforts I have tried to change your code to have it include the correct notify<operation>() at the right moment:


    private void updateMyAdapter() {

        ModelA modelA = CManager.getInstance().getModel(mAModelID);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

        if (recyclerView == null || modelA == null) {
            return;
        }

        Activity activity = getActivity();

        if (recyclerView.getAdapter() == null) {

            recyclerView.setAdapter(new RecyclerViewMergeAdapter());
        } else {
            
clearAdapters();

        }


        if (mImageHeader != null) {
            // NB : mImageHeader is created onCreateView =>  View mImageHeader = inflater.inflate(R.layout.header_image, recyclerView, false);
            appendView(mImageHeader);
        }

        View modelHeading = null;

        List<ModelB> favModelBs = CManager.getInstance().getModelBForModelA(mAModelID);

        if (favModelBs != null && !favModelBs.isEmpty() && activity != null) {
            MyRecyclerAdapter myRecyclerAdapter = new MyRecyclerAdapter(favModelBs, mAModelID, DisplayMode.MODEL_HINT);
            myRecyclerAdapter.setItemClickListener(this);
            modelHeading = inflateListLabel(recyclerView, getResources().getString(R.string.modelHeading), true); // inflateListLabel function inflate layout
            if (isAdded()) {
appendView(modelHeading);
myRecyclerAdapter.notifyDataSetChanged();

                appendAdapter(myRecyclerAdapter);
}
        }

        List<String> groupingIds = modelA.getGroupingIDs();

        if (groupingIds != null && !groupingIds.isEmpty()) {
            for (String groupingId : groupingIds) {
                GroupingModel grouping = CManager.getInstance().FetchGrouping(mAModelID, groupingId);
                if (grouping != null) {
                    List<ModelB> modelList = grouping.getModelBList();
                    if (modelList != null && !modelList.isEmpty() && activity != null) {
                        MyRecyclerAdapter myRecyclerAdapter = new MyRecyclerAdapter(modelList, mAModelID, DisplayMode.MODEL_HINT);
                        myRecyclerAdapter.setItemClickListener(this);
                        if (isAdded()) {

                            appendView(inflateListLabel(recyclerView, grouping.getName(), updatePadding));

                            // DO NOT call notifyDataSetChanged() unless absolutely necessary!
//                            myRecyclerAdapter.notifyDataSetChanged();

                            appendAdapter(myRecyclerAdapter);
}
                    }
                }
            }
        }

        RecyclerViewMergeAdapter adapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();

        if (isAdded() && adapter != null) {
adapter.notifyDataSetChanged();
}
    }

    private void appendAdapter(MyRecyclerAdapter myRecyclerAdapter) {

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

        if (recyclerView != null) {

            RecyclerViewMergeAdapter adapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();

            if (adapter != null) {

                adapter.addAdapter(myRecyclerAdapter);

                adapter.notifyItemInserted(adapter.getItemCount());
            }

        }
    }

    private void appendView(View view) {

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

        if (recyclerView != null && view != null) {

            RecyclerViewMergeAdapter adapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();
            
if (adapter != null) {

                adapter.addView(view);

                adapter.notifyItemInserted(adapter.getItemCount());
            }

        }
    }

    



    private void clearAdapters() {

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        
if (recyclerView != null && isAdded()) {
            RecyclerViewMergeAdapter recyclerViewMergeAdapter = (RecyclerViewMergeAdapter) recyclerView.getAdapter();
            int itemCountToBeCleared = recyclerViewMergeAdapter.getItemCount();

            
recyclerViewMergeAdapter.clearAdapters();

            recyclerViewMergeAdapter.notifyItemRangeRemoved(0, itemCountToBeCleared);

        }
    }

I have modified your clearAdapters() method, removed notifyDataSetChanged() and added notifyItemRangeInserted() to your methods appendView() and appendAdapter(). Could you try it out and let me know how it behaves? I have been looking around the Web and multiple sources report that the IllegalStateException you keep encountering is due to not calling the correct notification method. Thanks and I hope you'll be able to make the merge adapter work!

@guruduttstay
Copy link
Author

Thank you @ronaldw for your great help :)

Finally we figure it out and fixed it.

I am using viewPager and in next fragment I had this code where it was adding same view twice ...
and I didn't focused in that fragment at all and looking in current active tab in ViewPager and took lot of time to find actual culprit :(

View separator = Utils.inflateViewLabel(getActivity(), recyclerView ………);
if(separator != null) {
    mergeAdapter.addView(separator);
    mergeAdapter.addView(separator);
}

It added same view twice to mergeAdapter and ...... :(

@freecsdn
Copy link

I meet the same Issue, but it does not solve my problem.Is there any answer?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants