RecyclerView SpanSizeLookup - HORRIBLE performance with a large data set - android

The setup - RecyclerView / GridLayoutManager / custom SpanSizeLookup. Nothing unusual, it's just a grid with headers that span the entire width.
However what I've noticed is that performance can severely degrade the more and more items you have in the adapter. I've profiled this and it is 100% because of GetSpanSize in the custom lookup. It's a basic function, but it seems to be called for every item on every frame when scrolling. Sure enough, if I take out my lookup, performance is great regardless of how many items I have. As a use case this could be thousands of items. I start to see performance problems after maybe 1,000 items.
This sounds awfully inefficient from Android's side of things. I've searched high and low for someone else with this problem, but can't seem to find anything.
Any ideas?
Thanks,
EDIT: Added the code, however this still happens even if I just return 1 instead of doing the actual lookup. This issue seems to be that this is called for EVERY item in the adapter EVERY time the list moves when scrolling.
public class SpanSizeLookup : GridLayoutManager.SpanSizeLookup
{
//
private GridLayoutManager LayoutManager;
private MyItemAdapter ItemAdapter;
//
// SpanSizeLookup
public SpanSizeLookup( GridLayoutManager layoutManager, MyItemAdapter itemAdapter )
{
LayoutManager = layoutManager;
ItemAdapter = itemAdapter;
}
// GetSpanSize
public override int GetSpanSize( int position )
{
switch ( ItemAdapter.GetItemViewType( position ) )
{
case TYPE_HEADER:
return LayoutManager.SpanCount;
case TYPE_ITEM:
return 1;
}
return -1;
}
}
// GetItemViewType
public override int GetItemViewType( int position )
{
if ( ItemList[ position ].GUID.Equals( Guid.Empty ) )
return TYPE_HEADER;
return TYPE_ITEM;
}

The problem is you're not also overriding GetSpanIndex. The DefaultSpanSizeLookup is essentially this
public class DefaultSpanSizeLookup : GridLayoutManager.SpanSizeLookup
{
public int GetSpanSize(int position)
{
return 1;
}
public int GetSpanIndex(int position, int spanCount)
{
return position % spanCount;
}
}
If you override GetSpanIndex with the same value, you'll get the same performance as if you used DefaultSpanSizeLookup.

Related

Android Recylerview doesn't render an item when it is moved to the end of the recyclerview whilst the end is onscreen

In my app I display a list of outfits in a 2 column GridLayout RecyclerView, and allow users to swipe an outfit to the side. Upon swiping, I update the viewIndex of the outfit in the database (an integer which it uses for sorting the result of the "get all outfits" query), which causes the query my LiveData (generated by Room) is watching to change and put that item at the end of the returned list. This in turn calls a setList method in my RecyclerViewAdapter which uses DiffUtil to update the list.
Everything works as expected in most cases. An item is swiped to the side, disappears, and if you scroll to the bottom of the RecyclerView you can find it again at the end.
However, when the position in the RecyclerView where this swiped item should appear (i.e. the bottom) is currently visible to the user, the item does not appear. If additional items are swiped while the end is still visible, they won't appear either.
Upon scrolling up and then back down, the items will now be in their proper places - it's fixed. I do not know why they are not rendered intially though - is this something to do with DiffUtil perhaps? It could also have to do with my solution to this bug, where I save and restore the state of the RecyclerView either side of the setList call to prevent it scrolling to the new location when the first item of the list is moved (see BrowseFragment below). I admit, I do not know exactly what that code does, I only know it fixed that problem. I tried commenting out those lines but it didn't affect the disappearing views.
How can I ensure the swiped items display immediately without requiring a scroll up? Below is a gif demonstrating the feature in use and then showing the problem (sorry for low quality, had to fit under 2MB).
Code in BrowseFragment.java where RecyclerView and Adapter are initialised:
RecyclerView outfitRecyclerView = binding.recyclerviewOutfits;
outfitsAdapter = new OutfitsAdapter(this, getViewLifecycleOwner(), this);
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(requireActivity(), GRID_ROW_SIZE);
outfitRecyclerView.setLayoutManager(layoutManager);
outfitRecyclerView.setAdapter(outfitsAdapter);
//observe all outfits
outfitViewModel.getAllOutfits().observe(getViewLifecycleOwner(), (list) -> {
//save the state to prevent the bug where moving the first item of the list scrolls you to its new position
Parcelable recyclerViewState = outfitRecyclerView.getLayoutManager().onSaveInstanceState();
//set the list to the adapter
outfitsAdapter.setList(list);
outfitRecyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
});
Room dao query used to generate the LiveData observed in outfitsViewModel.getAllOutfits()
#Query("SELECT * FROM outfits ORDER BY view_queue_index ASC")
LiveData<List<Outfit>> getAll();
setList method in OutfitsAdapter.java, where outfits is a private member variable containing the current list of outfits.
...
public void setList(List<Outfit> newList){
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new OutfitDiff(newList, outfits));
diffResult.dispatchUpdatesTo(this);
outfits = newList;
outfitsFull = new ArrayList<>(newList);
}
private class OutfitDiff extends DiffUtil.Callback {
List<Outfit> newList;
List<Outfit> oldList;
public OutfitDiff(List<Outfit> newList, List<Outfit> oldList) {
this.newList = newList;
this.oldList = oldList;
}
#Override
public int getOldListSize() {
if(oldList == null){
return 0;
}
return oldList.size();
}
#Override
public int getNewListSize() {
if(newList == null){
return 0;
}
return newList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
}

Is there a way to populate a RecyclerView row with more than one ViewHolder object? What should the adapter implement?

I am implementing a recycler view with two ViewHolder types. After creating the first item as first-type view holder, I have a list of items of the same, second type. Now, I would like to juxtapose the second type items in order to define a two-column recycler view list considering the second type. Is it possible to do that? I have no idea of what should be implemented in the adapter and honestly I have found no good suggestions here. I guess I might not post my adapter code, since I don't know whether it is possible to do what I aim, I hope a conceptual answer may be sufficient too. I have an image, made with a not good image editor of my smartphone, I hope it is clear:
I am late but I found out a solution.
I associate to the recyclerview a GridLayoutManager with span count = 2.
Since I have, at the top of the recyclerview, an EditText (I use it as a search bar), as you can see in the image, I have to set the lookup of the grid. This is done by setSpanSizeLookup(). So, in the onViewCreated() method of my fragment class that initializes the recyclerview (I have a Fragment hosting the recyclerview), I insert the following code:
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 2);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
int type = adapter.getItemViewType(position);
if (type == RecyclerAdapter.SEARCH)
return 2;
else
return 1;
}
});
recyclerView.setLayoutManager(gridLayoutManager);
where the part "if (type == RecyclerAdapter.SEARCH) return 2;" means that the view holder for the search bar is going to occupy one whole row, 2 grid cells (since the span = 2).
In my recyclerview adapter, the method getItemViewType is essential:
#Override
public int getItemViewType(int position) {
if (position == 0)
return SEARCH;
else
return DESCRIPTION;
}
where SEARCH and DESCRIPTION are two static final variables.

Android expandable recycler view with grid layout manager, with specific expanding behaviour

I want to use an expandable recyclerview with grid layout manager, but with specific expanding behaviour which expand to the full width of the screen.
Is there any way or maybe libraries to achieve this? Thank you.
You need to add a transformation in your ViewHolder. Check this post: link
I donĀ“t know if Christopher's link allows for the expandable to occupy the whole 2 grids. There might be a way using a combination of getItemViewType(), and setSpanSizeLookup().
#Override
public int getItemViewType(int position) {
if (position == positionOfClickedItem + 2){
return TYPE_EXPANDABLE;
} else {
return TYPE_CAR_ITEM;
}
}
Bind the data according to its position, and carry the position to your activity to set SpanSizeLookup. You would then have to notify the adapter of a change:
// Create a SpanSizeLookup which returns 2 grids span if its the expandable or 1 otherwise.
gridLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
if (position == positionOfClickedItem) {
return 2;
} else {
return 1;
}
}
});
I think the transformation answer works better but just in case the Span size is important.

Different card size

How can I make it so that in the recycleView there are differents size cards? (1x1, 1x2 and 2x1 where 1 is the card length)
enter image description here
You can create two view holders. One of them holds the two cards that are in the same row, the other holds the full row one. It would definitely look like the image you posted. For implementing the recycler view with multiple view holders check out this.
You can use GridLayoutManager with different span count.
Here is some example.
In activity:
//Initialize recyclerView and adapter before
GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
if (adapter.isHeader(position)) {
//Returns span count 2 if method isHeader() returns true.
//You can use your own logic here.
return mLayoutManager.getSpanCount()
} else {
return 1;
}
}
}
});
And add this method to your adapter class:
public boolean isHeader(int position) {
return position == 0;//you can use some other logic in here
}

ListView: grouping items by section

Taking for example Gmail App, on my Navigation Drawer, I want a ListView that is grouped by section, similar to inbox, all labels.
Is this behavior achieved by using multiple ListView separated by a "header" TextView (which I have to build manually obviously), or is this section-grouped behavior supported by the Adapter or ListView?
Don't use multiple ListViews, it will mess things up for the scroll.
What you describe can be achieve by using only one ListView + adapter with multiple item view types like this:
public class MyAdapter extends ArrayAdapter<Object> {
// It's very important that the first item have a value of 0.
// If not, the adapter won't work properly (I didn't figure out why yet)
private int TYPE_SEPARATOR = 0;
private int TYPE_DATA = 1;
class Separator {
String title;
}
public MyAdapter(Context context, int resource) {
super(context, resource);
}
#Override
public boolean areAllItemsEnabled() {
return false;
}
#Override
public int getItemViewType(int position) {
if (getItem(position).getClass().isAssignableFrom(Separator.class)) {
return TYPE_SEPARATOR;
}
return TYPE_DATA;
}
#Override
public int getViewTypeCount() {
// Assuming you have only 2 view types
return 2;
}
#Override
public boolean isEnabled(int position) {
// Mark separators as not enabled. That way, the onclick and onlongclik listener
// won't be triggered for those items.
return getItemViewType(position) != TYPE_SEPARATOR;
}
}
You just have to implement your own getView method for a correct rendering.
I am not sure exactly how the Gmail app achieves this behavior, but it seems as though you should work on a custom adapter. Using multiple list views would not be a productive way to approach this problem, as one wants to keep the rows of data (messages) together in single list items.

Categories

Resources