My outer RecyclerView
crashes either with
IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true...
or
IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
Like the title suggests I have an RecyclerView
in the list item layout of the first RecyclerView
. This layout is used to display messages and the
inner RecyclerView
to display attachments that come with the message. The inner RecyclerViews
visibility is set to either GONE
or VISIBLE
depending whether the message has any attachments or not. The simplified outer list item layout looks like this
ConstraintLayout
TextView
TextView
TextView
RecyclerView
And the part of the adapter that handles the inner RecyclerView
looks like this
private fun bindFiles(message: Message?) = with(itemView) {
if (message != null && message.attachments.isNotEmpty())
{
sent_message_attachments.setAsVisible()
sent_message_attachments.layoutManager = GridLayoutManager(this.context,Math.min(message.attachments.size,3))
sent_message_attachments.adapter = AttachmentAdapter(message.attachments)
sent_message_attachments.itemAnimator = null
sent_message_attachments.setHasFixedSize(true)
}
else{
sent_message_attachments.setAsGone()
sent_message_attachments.adapter = null
sent_message_attachments.layoutManager = null
}
}
The bug has something to do with the way I fetch the attachments in the inner adapter since once I disable the part that start the download process, everything is fine. There's no problem when loading images from the device, but once I start the download process, everything goes to hell. This is the part that handles images and kicks off the download process in the inner adapter. I have functions for videos and for other file types that are pretty much the same exact thing but use slightly different layout.
private fun bindImage(item: HFile?) = with(itemView) {
if (item != null)
{
if (item.isOnDevice && !item.path.isNullOrEmpty())
{
if (item.isGif)
{
attachment_image.displayGif(File(item.path))
}
else
{
attachment_image.displayImage(File(item.path))
}
}
else
{
//TODO: Add option to load images manually
FileHandler(item.id).downloadFileAsObservable(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
//TODO: Show download process
},
{ error ->
error.printStackTrace()
//TODO: Enable manual retry
},
{ notifyItemChanged(adapterPosition)} //onComplete
)
}
}
}
I use the same structure as above in my DiscussionListAdapter
to load discussion portraits (profile pictures etc.) and it does not have the same issue.
These are the extensions functions used to inflate the viewHolders and to display the images
fun ViewGroup.inflate(layoutRes: Int): View
{
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}
fun ImageView.displayGif(file:File){
GlideApp.with(context).asGif().load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
fun ImageView.displayImage(file:File){
GlideApp.with(context).load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
I've been on this for the past couple of days and just can't get my head around it. Any help in any direction is greatly appreciated. I know my explanations can be a bit all over the place so just ask for clarification when needed :)
I have now been able to produce this with a GridLayout
as well as with RecyclerView
. It's safe to assume that the nested RecyclerViews
were not the culprit here. I even tried to ditch the Rx-piece that handled loading the images and created an IntentService
for the process, but the same crashes still occur.
With GridLayout
I mean that instead of having another adapter to populate the nested RecyclerView
I use only one adapter to populate the message and to inflate and populate views for the attachments as well and to attach those views to the nested GridLayout
.
The crash happens when I start to download a file and then scroll the view, that is supposed to show the downloaded file, out of the screen. That view should get recycled but for some reason the download process (which in my test cases only takes around 100ms-400ms) causes the app to throw one of the two errors mentioned in the original question. It might be worth noting that I'm using Realm
and the adapter takes in a RealmResults<Message>
list as it's dataset. My presenter looks for changes in the list and then notifies the adapter when needed (changed due to the implementation of IntentService
).
This is how I'm capable to reproduce this time and time again:
There is no crash if I stop and wait for the download to complete and everything works as intended. The image/video/file gets updated with a proper thumbnail and the app wont crash if I scroll that out of view.
I tried swapping the nested ViewGroup
for a single ImageView
just to see is the problem within the nestedness. Lo and behold! It still crashes. Now I'm really confused, since the DiscussionListAdapter
I mentioned before has the same exact thing in it and that one works like a charm... My search continues. I hope someone, some day will benefit from my agony.
I started to log the parent of every ViewHolder
in the onBindViewHolder()
function. Like expected I got nulls
after nulls
after nulls
, before the app crashed and spew this out.
04-26 21:54:50.718 27075-27075/com.hailer.hailer.dev D/MsgAdapter: Parent of ViewHolder: android.view.ViewOverlay$OverlayViewGroup{82a9fbc V.E...... .......D 0,0-1440,2168}
There's a method to my madness after all! But this just poses more questions. Why is ViewOverlay
used here? As a part of RecyclerView or as a part of the dark magicians plans to deprive me of my sanity?
Sidenote
I went digging into RecyclerViews
code to check if I could find a reason for the ViewOverlay
mystery. I found out that RecyclerView
calls the adapters onCreateViewHolder()
function only twice. Both times providing itself as the parent
argument for the function. So no luck there... What the hell can cause the item view to have the ViewOverlay
as it's parent? The parent is an immutable value, so the only way for the ViewOverlay
to be set as the parent, is for something to construct a new ViewHolder
and supply the ViewOverlay
as the parent object.
Sometimes I amaze myself with my own stupidity. The ViewOverlay
is used because the items are being animated. I didn't even consider this to be an option since I've set the itemAnimator
for the RecyclerView
as null
, but for some odd reason that does not work. The items are still being animated and that is causing this whole charade. So what could be the cause of this? (How I chose to ignore the moving items, I do not know, but the animations became very clear when I forced the app to download same picture over and over again and the whole list went haywire.)
My DiscussionInstanceFragment
contains the RecyclerView in question and a nested ConstraintLayout that in turn contains an EditText
for user input and a send button.
val v = inflater.inflate(R.layout.fragment_discussion_instance, container, false)
val lm = LinearLayoutManager(context)
lm.reverseLayout = true
v.disc_instance_messages_list.layoutManager = lm
v.disc_instance_messages_list.itemAnimator = null
v.disc_instance_messages_list.adapter = mPresenter.messageAdapter
This is the piece that handles the initialization of the RecyclerView
. I'm most definitely setting the itemAnimator
as null
, but the animations just wont stop! I've tried setting the animateLayoutChanges
xml attribute on the root ConstraintLayout
and on the RecyclerView
but neither of them worked. It's worth mentioning that I also checked whether the RecyclerView
had an itemAnimator
in different states of the program, and every time I check the animator, it is null. So what is animating my RecyclerView
?!
I finally figured out what was causing this. In my DiscussionInstanceView
I have a small view
that is animated into and out of view with ConstraintLayout
keyframe animations. This view only shows the download progress of the chat history and is used only once, when the discussion is first opened. BUT since I had a call to hiding that view every time my dataset got updated, I was forcing the ConstraintLayout
to fire of an animation sequence thus making everything animate during the dataset update. I just added a simple check whether I was downloading the history or not and this problem got fixed.