davideas / FlexibleAdapter

Fast and versatile Adapter for RecyclerView which regroups several features into one library to considerably improve the user experience :-)
Apache License 2.0
3.55k stars 548 forks source link

ClassCastException after updateDataSet with StickyHeaders #696

Open technicalflaw opened 5 years ago

technicalflaw commented 5 years ago

I'm having an issue when I update the adapter's data set (via updateDataSet()), but only when sticky headers are enabled, AND a new header is introduced as part of the update, AND a scroll occurs, such that previously sticky header's position changes. I'm not using expandable headers. In addition, it's very hard to reproduce. It can take hours, but eventually it will occur. Here's the stack:

java.lang.ClassCastException: com.myapp.MyHeaderItem$ViewHolder cannot be cast to com.myapp.MySectionedItem$ViewHolder
        at com.myapp.MySectionedItem.unbindViewHolder(AbstractSegmentItem.java:61)
        at eu.davidea.flexibleadapter.FlexibleAdapter.onViewRecycled(FlexibleAdapter.java:1816)
        at eu.davidea.flexibleadapter.helpers.StickyHeaderHelper.swapHeader(StickyHeaderHelper.java:245)
        at eu.davidea.flexibleadapter.helpers.StickyHeaderHelper.updateHeader(StickyHeaderHelper.java:162)
        at eu.davidea.flexibleadapter.helpers.StickyHeaderHelper.updateOrClearHeader(StickyHeaderHelper.java:138)
        at eu.davidea.flexibleadapter.helpers.StickyHeaderHelper.onScrolled(StickyHeaderHelper.java:64)
        at androidx.recyclerview.widget.RecyclerView.dispatchOnScrolled(RecyclerView.java:4961)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4021)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3652)
        at androidx.recyclerview.widget.RecyclerView.consumePendingUpdateOperations(RecyclerView.java:1888)
        at androidx.recyclerview.widget.RecyclerView$1.run(RecyclerView.java:407)
    ...
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:767)
        at android.view.Choreographer.doCallbacks(Choreographer.java:580)
        at android.view.Choreographer.doFrame(Choreographer.java:549)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:753)
        at android.os.Handler.handleCallback(Handler.java:739)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:135)
        at android.app.ActivityThread.main(ActivityThread.java:5343)
        at java.lang.reflect.Method.invoke(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:372)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:905)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:700)

As you can see, when the StickyHeaderHelper manually recycles its sticky header (normally via its scroll handler, as it scrolls out of the view), it thinks its item is a MySectionedItem instance (i.e. a subclass of AbstractSectionableItem, not AbstractHeaderItem. So the ViewHolder passed to AbstractSectionableItem#unbindViewHolder() is that of the header.

As far as I can tell (I'm in no way sure), this is because the StickyHeaderHelper keeps a reference to the ViewHolder for the stuck item, so when the data set is updated, and the time comes to swap out them item (i.e. recycle it), and it requests its position in the adapter (via FlexibleAdapter#onViewRecycled()'s call to RecyclerView.ViewHolder#getAdapterPosition()) it receives the position it was at before the update, which is still a valid index, but no longer referencing the header's actual position in the new data set.

I should also add that my data set is unusual in that its updates tend to be at the head of the list, so new headers tend to be created at index 0, thus offsetting all items after it. If headers were to be created at the end of the list it would be less likely to occur.

As I say, I'm not 100% sure my explanation is correct, but it's the best I can think of. It might also be related to FlexibleAdapter.AdapterDataObserver#updateStickyHeader()'s delayed call to StickyHeaderHelper#updateOrClearHeader(), which might explain why it doesn't happen every time.

I've checked that my item layout ids are unique, and that Object#equals works OK. I think it might be related to issues #568 and #575. Any help (or just a workaround) would be greatly appreciated.

davideas commented 5 years ago

@technicalflaw, I don't have time to analyse all this. Can you debug it? Are you scrolling down I guess, so the sticky header will disappear at top, because the view is indeed recycled manually. Please check if the position at line 1814 in FlexibleAdapter.onViewRecycled() is a valid one and belongs to the expected Item and ViewHolder. Which is the position of the sticky header and which the position found?

technicalflaw commented 5 years ago

Sorry for the delay. I've had another look, and whilst I've 'fixed' it I don't know if it's fixing a problem I caused, or something else. Anyway, the main issue seems to be FlexibleAdapter.onViewRecycled() referencing the adapter position of items that have moved, or been deleted, depending on when it is called. If it's called from StickyHeaderHelper when it manually recycles its ViewHolder it's very likely to be wrong, because that class doesn't listen for data change events, but I've also had it happen via delayed recycling after a layout (see RecyclerView#mUpdateChildViewsRunnable).

That said, here's how I've worked around it.

//            if (mHeaderPosition > oldHeaderPosition) {
//                mAdapter.onViewRecycled(mStickyHeaderViewHolder);
//            }

This will need a lot of testing (a previous solution seemed to work for a couple of days, but ultimately failed), and I'm still not sure if it's right. So far, so good, however.

davideas commented 5 years ago

@technicalflaw thank you. I should have a look, but I do not know when: I have important nicely events in my life now :-D

PS. The ideal is to get over of this solution and develop a custom LayoutManager (solution more complicated but correct).