The documentation says that setItemViewCacheSize
sets the number of offscreen views to retain before adding them to the
potentially shared recycled view pool.
and setMaxRecycledViews
sets the maximum number of ViewHolders to hold in the pool before
discarding.
But don't they both function as a cache where views are taken from (i.e., the first sets the number of views cached by the RV, while the second sets that of the RVP)?
Also, when a view is needed, where is it taken first, from the RVP or from the RV's cache?
And what's the optimal (scrolling-wise, ignoring memory) configuration for the two for a simple unnested recyclerview?
Here's the full documentation for setItemViewCacheSize():
Set the number of offscreen views to retain before adding them to the potentially shared recycled view pool.
The offscreen view cache stays aware of changes in the attached adapter, allowing a LayoutManager to reuse those views unmodified without needing to return to the adapter to rebind them.
In other words, when you scroll the RecyclerView such that there's a view that is just barely completely off-screen, the RecyclerView will keep it around so that you can scroll it back into view without having to re-execute onBindViewHolder().
This is different from the recycled view pool, which is a pool of views that the RecyclerView has said it doesn't need anymore, but which is kept around to avoid the expensive task of inflating new views.
In short, the "item view cache" holds elements in such a way that the RecyclerView can avoid calling both onCreateViewHolder() and onBindViewHolder(), whereas the recycled view pool holds elements such that the RecyclerView can avoid calling onCreateViewHolder() but will still have to call onBindViewHolder().
Also, when a view is needed, where is it taken first, from the RVP or from the RV's cache?
I don't think it really matters, and I don't know of a precise definition, but generally I think you can imagine that views that have just exited the device's viewport and then return to the viewport will be taken from the "item view cache", while views that come on-screen but were not previously on-screen will come from the recycled view pool.
And what's the optimal (scrolling-wise, ignoring memory) configuration for the two for a simple unnested recyclerview?
Just use the defaults. I would never consider changing these unless I had profiled my app and determined beyond a doubt that the defaults weren't working for me. But, if I just take you at your word, ignoring memory, the larger the cache size the better. But really, just use the defaults.
I've read this article by Pavel Shmakov and it explains the difference between Pool and Cache
Pool and Cache in Action
If a ViewHolder was found nowhere, it is created and bound.
If a ViewHolder was found in pool, it is bound.
If a ViewHolder was found in cache, there is nothing to be done.
So, as long as the cache isn’t full, ViewHolders go there. If it’s
full, a new ViewHolder pushes a ViewHolder from the “other end” of the
cache into the pool. If a pool is already full, that ViewHolder is
pushed into oblivion, to the garbage collector that is
Now let’s look at the way pool and cache behave in a couple of actual
RecyclerView usage scenarios.
Consider scrolling:
As we scroll downwards, there is a “tail” behind the currently seen
items consisting of cached items and then a pooled item. When the item
8 appears on screen, no suitable ViewHolder is found in cache: no
ViewHolder associated with position 8 there. So we use a pooled
ViewHolder, which was previously at position 3. When item 6 disappears
on top, it goes to the cache, pushing 4 into the pool.
The picture is different when we start scrolling in the opposite
direction:
Here we find a ViewHolder for position 5 in view cache and reuse it
right away, without rebinding. And that seems to be the main use-case
of the cache — to make scrolling in opposite direction, to the items
we’ve just seen, more efficient. So if you have a news feed, the cache
might be useless, since users won’t go back too often. But if it’s a
list of things to choose from, say a gallery of wallpapers, you might
want to extend the capacity of the cache.
Read more here https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714
Related
I have a problem with the RecyclerView. I need to set the recycler flag viewHolder.setIsRecyclable(false) to false for some important reasons (ViewModel, LiveData,..). When I add a lot of items to the list, the app works fine, the memory does not raise dramatically, but when i swipe up and down the memory increase dramatically. My Questions: Why does memory consumption increase when swiping? The views are already added, so why ist the memory increasing?
From Documentation
fun setIsRecyclable(recyclable: Boolean): Unit
Informs the recycler whether this item can be recycled. Views which
are not recyclable will not be reused for other items until
setIsRecyclable() is later set to true. Calls to setIsRecyclable()
should always be paired (one call to setIsRecyclabe(false) should
always be matched with a later call to setIsRecyclable(true)). Pairs
of calls may be nested, as the state is internally reference-counted.
The recycler view basically destroys the views and recreates them when it is necessary (within the user-screen-visible range). Since you set viewHolder.setIsRecyclable(false) the views won't be recycled leads to increase in the memory.
Q: The views are already added, so why is the memory increasing?
The views already added are not destroyed and again new views are created when you swipe up which leads to increase in the memory.
viewHolder.setIsRecyclable(false) is not the solution for your expected result.
I have a RecyclerView (1.2.1), with a List adapter, and ViewHolder, backed by a Room PagingSource. There's about 700 items on the list. The paging seems to work fine, and I've flattened my View hierarchy as much as I can.
Upon initially loading the recycler view, everything seems fine. Paging works, everything seems snappy. onCreateViewHolder in my adapter is called 14 times, and initially 5 are visible on the screen.
Slower scrolling is fine (it does call onCreateViewHolder more often than I expected, but there's no jank).
The problem comes when rapidly flinging through the list. After 3-5 fast flings, it appears to decide that it needs to have more cached view holders, and makes many, many calls to onCreateViewHolder - this method is clocking in at ~5ms, but there's just too many of them, and the scrolling stops. It appears to call onCreateViewHolder ~700 times - the same as the number of items on the list, like it's not recycling the views at all.
At that point, sometimes the app will recover, and at that point everything is smooth and it doesn't appear to need to create more ViewHolders. Sometimes however I will get the ANR dialog.
I've tried tweaking the recyclerView.recycledViewPool.setMaxRecycledViews(), but this doesn't appear to to increase the recycledViewCount until after the mass onCreateViewHolder calls.
Is there anything I can do to resolve this? Make the fling speed slower? Tune the view holder recycling somehow so that it doesn't go nuts and try and create so many at once?
I don't think I can get the layout inflation any better, given my design and data constraints. And even if I could, it's still creating waaay too many to be able to get them done in under 16ms!
In my situation, I needed to do 2 things. First and foremost, I needed to adjust my paging configuration. My prefetchDistance was too small in relation to my page size! This got rid of the ANR - no more mass creation of ViewHolders!
The recyclerView would still pause scrolling when loading a page (especially towards the end). I added a loadStateFlow collector that handles showing a loading indicator so that the user knows there's more data coming.
Background
I'm using the PinterestLikeAdapterView library to show some images from the internet, which is like a gridView but with different height for each cell.
The problem
Since I use this library to show images from the internet, it's crucial that when calling notifyDatasetChanged won't cause a mess on the views.
For some reason, calling this function would call the getView() method with different positions for the views. for example, even though i didn't scroll at all, and call notifyDatasetChanged (or addAll in case it's an ArrayAdapter), for position 0 it will take what was the view of position 8, for position 1 it will take the view of position 7 , and so on...
This makes the whole grid to refresh its images, and so it ruins the UX.
Usually, in both gridView and listView, the way to overcome refreshing is to put the position that was used for the view inside the viewHolder, and if they are equal, it means that they still match.
for example:
... getView(...)
{
//<=inflate a new view if needed
//avoid refreshing view in case it's still the same position:
if(position==holder.position)
return rootView;
holder.position=position;
//<=update the view according to its data
...
}
However, here they re-use other views in a different order so this trick won't work here.
Because of this issue, not only i get refreshes of almost all of the visible views, but since i use DiskCacheLru library, it crashes since it tries to put 2 identical inputSteam data into the same key using 2 threads.
The question
What can I do?
Is this a known bug in the library?
Maybe I'm using a bad way to overcome refreshes?
for now, i use memory cache to at least get items that were cached before, but that's more like a "cure" than a "vaccine"...
Short answer:
Use an image loading library like Picasso that caches most recently used images in memory, so they don't need to be reloaded from the network.
Long answer:
AdapterView does something called View recycling, where Views which are no longer needed to display a position are re-used to display another. (For example, as you scroll down, Views that disappear off the top of the screen are reused for new positions at the bottom of the screen.) Because of this, it's normal for getView() to be passed the same View for more than one position.
This is done for performance reasons: Inflating new Views is hard and takes time, so AdapterView tries to do it as infrequently as possible.
When using a holder, you store references to ImageView and TextView children inside the item's View, so you don't have to look them up with findViewById() each time - you don't usually store anything specific to a particular position, because the View and its holder will often be used for different positions.
Now, when you call notifyDataSetChanged(), AdapterView assumes that the data set has completely changed. The image that was associated with position 8 may no longer be present, or it may be associated with position 12 now. Consequently, all the existing Views are scrapped - but because AdapterView would still like to avoid inflating new Views, they're re-used to display the new data, with no regard for what position they were displaying previously.
This explains why getView() is being passed the same View for different positions, and why visible positions are being refreshed when you call notifyDataSetChanged(). But how to avoid having your images refresh, ruining the user experience?
Use an image loading library like Picasso that caches most recently used images in memory, so they don't need to be reloaded from the network. The refresh will still happen, but it'll be instantaneous.
View getView(int position, View view, ViewGroup parent) will be always called ascendingly, after notifyDataSetChanged().
I guess that, the order of finishing download task will cause this problem.
As you mentioned in your question, keeping the position is a good way to avoid this problem.
Here is another way to solve it, also re-use the imageviews.
Keep a weak reference of each ImageView in download task.
Then wrap the download task in a dummy ColorDrawable.
When getView is called, set the dummy ColorDrawable to ImageView, and start the download. When download is complete, set the downloaded image back to the referenced ImageView in OnPostExecute().
Explanation
http://android-developers.blogspot.jp/2010/07/multithreading-for-performance.html
Source code
https://code.google.com/p/android-imagedownloader/source/checkout
There is a very good example on PinterestLikeListView in GitHub
Here is the library StaggeredGridView
A modified version of Android's experimental StaggeredGridView. Includes own OnItemClickListener and OnItemLongClickListener, selector, and fixed position restore.
You can get library project here library
and you can get Demo project Here
This is very good open source project, so you can use instead of PinterestLikeAdapterView
Hope this library is going to help you out.
seems that the authors of this library have fixed it, after some time i've reported about it:
https://github.com/huewu/PinterestLikeAdapterView/issues/8
I'm trying to show an animation with all Views that I've created from an adapter. When I scroll down, it shows the animation correctly, but when I scroll up, I see these Views recreate themselves and show the animation again. Then, when I scroll down, it happens again.
My assumption is that the mechanism of creating a View from an adapter is to load the View into memory; just the group of Views which are on screen right now (but above and below views are not loaded into memory). These will be loaded again when I scroll to these views, right?
Is there any way to fix this problem?
PS: Sorry for my English, I hope you understand my problem.
My assumption is that the mechanism of creating a View from an adapter
is to load the View into memory; just the group of Views which are on
screen right now (but above and below views are not loaded into
memory)
That's somewhat correct: a ListView will not try to visualize any data that isn't (at least partially) visible. It also 'recycles' views, meaning that any view that isn't currently used to present data to the user and is of the same 'type' as the next data item, may get reused.
Hence you shouldn't rely on persisting data with or make any assumptions about the existence of particular views. In stead, use something that's separate from the views; e.g. the dataset you're visualizing.
Quite often, you'll supply a list of POJOs to a BaseAdapter or ArrayAdapter. You could simply add a boolean to the POJO indicating whether it should animate or not, and change that whenever the animation for that particular item finishes. Alternatively, you could keep track of these values in a separate collection (which is probably the more straightforward approach if you're dealing with a Cursor as data source rather than POJOs).
When overriding the baseadapter on an android listview, you have to implement this method public View getView(int position, View convertView, ViewGroup parent). The convertview is the view that was previously pushed off the list when scrolling, and it's given so that you can reuse that view instead of creating a new view.
My question is, is it really necessary to reuse the view? I can understand reusing it if only a piece of the data is changed. But is the overhead of creating a view really THAT significant? Every tutorial on using listviews I've seen tells you to recycle the view, even on trivially simple views like a textview.
I guess my question is why did google decide to make this the default behavior of the getView method?
A couple of reasons to recycle views:
Object creation is relatively expensive. Every additional object that is created needs to be dealt with by the garbage collection system, and at least temporarily increases your memory footprint
This is more important for more complex views, but inflating and laying out the view objects can be expensive. Most often, you are only making minor changes to the view in getView that won't affect the layout (e.g, setting text) so you might be able to avoid the layout overhead.
Remember that Android is designed to be run in a resource constrained environment.
Finally, its already done for you, and it certianly doesn't hurt anything, so why not use it?
Is it necessary? Only if you like an extra 30-40 fps during flings on a Nexus One. :) (See the slides from http://code.google.com/events/io/2010/sessions/world-of-listview-android.html, slides 13-17)
Why make the device do work that it doesn't need to do by ignoring a significant optimization that's been 95% done for you?