I am using a Recyclerview to display list of images and other content. When the recyclerview is loaded and scrolled quickly then it is crashing.
When I debug I could see that
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
...
viewHolder.getItemId() // this returns -1, sometimes
I suspect that the Recyclerview Adapter views are not created fully, any suggestions for this?
As you have not shared the code, We are not sure whats your case but let me share a possible cases that might be your case.
Possible Cases for RecyclerView Crash:
1. This crash might be caused by the fact that the setViewHolder and onBind
methods are being called while your list(used by the adapter) is being updated, but notifyDataSetChanged() hasn't been called.
2. This problem may caused when android start dealing with bitmaps. the main
reason is lack of memory space and also, garbage collector cannot free some
space.
3. If you using image , please confirm the image size because the size may
cause this problem. Some case recyclerview was not able to hold the images
with big resolution.
4. Double check if you are using all the data loading process in ( main/UI
thread ), it my testing Looper.getMainLooper() == Looper.myLooper().
Related
This is just a mock to give you some idea of what I am doing.
My recycler view has complex logic. Let me point out them,
View holder UI is complex.
Loading banner ads after each 5 view holders.
My data is coming from network and I have been using Room + Retrofit + Paging adapter.
User experience is very bad. I need some suggestions. I believe there are 2 things effecting my scrolling function.
xml ui inflation.
Loading admob ads in UI thread.(They want us to do it in ui thread. still I dont know why they do this crazy stuffs. )
I need some suggestions how can I improve and give some good user experience .
Since there is no source code, I'm unable to try to evaluate the exact cause of this. But I experienced something like this before, I have some possible solution for you.
1) Move long-running task away from UI thread:
I noticed that your data is coming from Room + Retrofit. By default, Room must operate in async manner, unless allowMainThreadQueries() is called. If you did called allowMainThreadQueries(), you can check your code if you accidentally trying to fetch data on UI thread.
2) Did you implement RecyclerView (RV) properly?
RV reduces the amount of xml inflation whenever possible and improve performance by reusing the inflated layout for the same view type. So, if you only have 2 view types as shown in your question, RV will only inflate 3-6 of your layout (even if you have e.g. 100 items in your list) and attaches it to ViewHolder and then bind and recycle the view with Item as you scrolled through the list.
However, RV may perform poorly if implemented wrongly. One example I experienced before is returning position as view type in RV adapter.
public class SampleAdapter extends RecyclerView.Adapter<RVItem.ViewHolder> {
private final List<Item> items = new ArrayList<>();
#Override
public int getItemViewType(int position) {
return position; //Never do this
return items.get(position).getType(); //Do this
}
}
So, you can try to check if any of your implementation/logic is wrong with RV.
3) Use Profiler in Android Studio
If none of the above suggestions resolve the issue. The last way I can think of is recording the trace using Profiler while u scrolling through the RV and trying to identify which call is time-consuming/blocking by analysis the trace.
More info: https://developer.android.com/studio/profile/android-profiler
Additional info
Lastly, may I know what do you mean by loading AdMob ads in UI thread. As far as I know, AdMob load and return ads asynchronously like UnifiedNativeAd. And only then you try to inflate and set your view using the data from UnifiedNativeAd on UI thread which is not really UI-blocking task
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 :)
UPDATE
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:
Open a discussion that has messages with attachments
Start to scroll upwards for more messages
Pass a message with an attachment and scroll it off screen while it's still loading
Crash
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.
UPDATE 2
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.
UPDATE 3
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 ViewOverlaymystery. 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.
UPDATE 4
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 have faced the same issue
Try this in your child RecyclerView it works for me
RecyclerView childRC = itemView.findViewById(R.id.cmol_childRC);
layoutManager = new LinearLayoutManager(context);
childRC.setItemAnimator(null);
childRC.setLayoutManager(layoutManager);
childRC.setNestedScrollingEnabled(false);
childRC.setHasFixedSize(true);
now set your Adapter like this
ArrayList<Model> childArryList = new ArrayList<>();
childArryList.addAll(arrayList.get(position).getArrayList());
ChildOrderAdapter adapter = new ChildOrderAdapter(context, childArryList);
holder.childRC.swapAdapter(adapter, true);
hope this helps
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.
I am building a gallery using a ViewPager. Every time a photo is downloaded I immediately cache it in mMemoryCache which is
mMemoryCache = new LruCache<Integer, Bitmap>(cacheSize) {
#Override
protected int sizeOf(Integer key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number
// of items.
return (bitmap.getRowBytes() * bitmap.getHeight());
}
};
As you can see sizeOf() returns the number if bytes used, which makes sense. My problem is that my ViewPager's adapter needs to know how many pages I have, which would be the number of objects in my cache.
Any ideas on how I can do it? thanks!
I think you should rethink your approach. Usually, when you use some kind of paging (i mean data paging, not the ViewPager), you first add a certain fixed number of items to your Adapter. You can either start downloading the required resources right away, or wait until you really need them. Check in Adapter.onCreateView() if the resource is already downloaded and cached. If yes, get it from there, if not, start an asynchronous download and add the image to the view as soon as you got it. Show a placeholder in the meantime.
When you reach the last item in your adapter, add another batch to it and everything starts over again.
If you really want to know what items are in your LRUCache, override put() and entryRemoved(). There you know what items get added or evicted from the cache. I would advise against this method though.
I've implemented OnScrollListener to understand if the user is at the end of the list and I've managed to successfully appended new data to my list as follows:
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == SCROLL_STATE_IDLE) {
if (list.getLastVisiblePosition() >= list.getCount()-1) {
currentPage++;
loadMore(currentPage);
adapter.notifyDataSetChanged();
}
}
}
The problem is notifyDataSetChanged refreshes the whole ListView as expected. But since in my list there are images that are asynchronously loaded from the internet, this refresh causes the images to load again and this is obviously not desired. The applications like Facebook, Twitter etc they seamlessly attach new data at the end without this kind of refreshing at all. How do they do it, how can I also achieve this seamless endless scroll?
I think I have to do something in my adapter's getView method to prevent reload but I could not figure it out yet. Is there a known way for it?
Thanks!
You are going to want to implement an image cache. The cache should be asked for the images and either load them asynchronously from the internet or load them from memory if used before. The cache should be used in getView.
The simplest way to do a cache is to have a hashmap (probably a ConcurrentHashMap if you are getting the images async on a separate thread), where the keys are the url and the value is the image you want to show. You can then check if the hashmap has the value first, otherwise get the image and insert in the hashmap as well. There are many other ways to do this if you look around, but this is probably the easiest.
This solution is more robust, as you do not need to worry about what to invalidate and you do not need to worry about making more than one call when requesting the same image.
I been fighting an odd issue these last few days. I have a custom ExpandableListAdapter where each row contains an ImageView, among other things. I have a class that handles the asynchronous loading of images from the multitude of places they may reside (disk cache, app data, remote server, etc). In my adapter's getView method I delegate the responsibility of returning a View to the list Item itself (I have multiple row types for my group list). I request the image load as follows:
final ImageView thumb = holder.thumb;
holder.token = mFetcher.fetchThumb(mImage.id, new BitmapFetcher.Callback() {
#Override
public void onBitmap(final Bitmap b) {
thumb.post(new Runnable() {
#Override
public void run() {
thumb.setImageBitmap(b);
}
});
}
#Override
public void onFailure() {
}
});
Yeah, it's ugly, but I decided against some contract where you have the BitmapFetcher.Callback execute its methods on the UI thread by default.
Anyway, when I load the Activity that contains the ExpandableListView there will often be thumb images missing from different rows in the list. Reloading the Activity may cause some of the missing thumbs to show but others that were previously showing may not be anymore. The behavior is pretty random as far as I can tell. Scrolling the ListView such that the rows with missing images get recycled causes the new thumb images (when the recycled row gets displayed again) to load fine. Scrolling back to rows that previously contained missing images causes the missing images to appear. I can confirm that all the images are loading correctly from my BitmapFetcher (mFetcher) class. I should also mention that I load other images in other places. Every once in awhile they don't appear either.
After pulling most of my hair out, I discovered that changing:
thumb.post(new Runnable() {
to:
mExpListView.post(new Runnable() {
fixes the issue. I originally thought that the issue might be happening because I was using a final reference to a View, but the other locations in the app use non-final references to a view to post messages, and, as I mentioned, sometimes those did not work. I eventually changed everything to use an Activity's runOnUiThread() method (and my own getUiThreadRunner().execute method when inside Fragments) and that seems to fix the issue all around.
So my question remains, in what cases can View.post() to fail to deliver the runnable to the associated ViewRoot's message queue in the proper order? Or, perhaps the invalidate() is happening before the View is returned from getView and thus before it's placed in a ViewGroup that can be reached from the root View. Those are really the only cases I can think of that would prevent the image from showing up. I can guarantee that none of these calls are happening until at least onStart has finished executing. Further, it looks like it's fine to post to a View even if it hasn't been attached to a Window yet:
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(attachInfo.mHandler);
(in performTraversal). The only difference between the runOnUiThread and post seems to be that an Activity has a different Handler than the ViewRootImpl.
Activity:
final Handler mHandler = new Handler();
whereas in ViewRootImpl:
final ViewRootHandler handler = new ViewRootHandler();
But, this should not be a problem provided both Handlers were constructed in the same Thread (or using the same Looper). That leaves me wondering if it is, indeed, a problem to invalidate() a View that has not yet been added to the hierarchy. For this to be the case invalidate should either 1. not do anything if it's not visible, or 2. only be valid for the next performTraversal() that happens.
View.invalidate() checks a nice private method that's not documented called skipInvalidate():
/**
* Do not invalidate views which are not visible and which are not running an animation. They
* will not get drawn and they should not set dirty flags as if they will be drawn
*/
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
It looks like number 1 is more accurate! However, I would think this only pertains to a View's VISIBILITY property. So, is it accurate to assume that a View is considered not VISIBLE if it cannot be reached from the ViewRoot? Or is the VISIBILITY property unaffected by the View's container? If the former is the case (which I suspect it is) it raises a concern. My use of Activity.runOnUiThread is not a solution to the problem. It only happens to work because the invalidate() calls are being sent to a different Handler and being executed later (after getView returns and after the row has been added and made visible on the screen). Has anybody else run into this issue? Is there a good solution?
Hey David I ran into a similar issue long time back. The basic requirement for view.post(Runnable r) is that the view should be attached to the window for Runnable to be executed. However, since you are loading images asynchronously in your first case, therefore there is a probability that imageView aren't attached to window when post request is made and hence, some images fail to load.
Quoting earlier version of docs on the same:
View.post() : Causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread. This method can
be invoked from outside of the UI thread only when this View is
attached to a window.
Switching to you next question, what is the best solution to handle this situation ?
Can't comment on the best solution. However, I think both handler.post() and activity.runOnUIThread() are good to go. Since, they basically post runnable in main thread queue irrespective of anything and in general, the request to display list rows would be enqueued prior to our thumb.post(). So, they might work flawlessly for most cases. (Atleast I've never faced a problem with them !). However. if you find a better solution, do share it with me.
Try this : setBitmap() like this :
runOnUiThread(new Runnable() {
#Override
public void run() {
thumb.setImageBitmap(b);
}
});