4

Issue Summary

I have a RecyclerView list in an activity with an actionBar button used to switch the RecyclerView between a LIST and a GRID layout.

The layouts of the items are different when viewing the list as a LIST and as a GRID.

When viewing the items in a LIST the row format is

|----|
|icon|  Item Name
|----|

When viewing the items in a GRID the row format is

|--------------|
| large icon   |
|              |
|              |
|              |      
|--------------|
    Item Name

Therefore viewing my list as i LIST i see

|----|
|icon|  Item1
|----|
|----|
|icon|  Item2
|----|
|----|
|icon|  Item3
|----|
|----|
|icon|  Item3
|----|

and when viewing my list as a GRID i see

|--------------|    |--------------|
| large icon   |    | large icon   |      
|              |    |              |      
|              |    |              |      
|              |    |              |      
|--------------|    |--------------|
    Item1                Item2
|--------------|    |--------------|
| large icon   |    | large icon   |
|              |    |              |
|              |    |              |
|              |    |              |      
|--------------|    |--------------|
    Item3                Item4

When i use the actionBar button to toggle the list view from LIST to GRID or GRID to LIST, some views are still rendered in the layout for the other style. I believe its the views which you scrolled out of sight before switching are recycled and reused rather than recreated.

For example if i go from GRID to LIST i see

                    |--------------|
                    | large icon   |      
|----|              |              |      
|icon|  Item1       |              |      
|----|              |              |      
                    |--------------|
                         Item2
|--------------|    |--------------|
| large icon   |    | large icon   |
|              |    |              |
|              |    |              |
|              |    |              |      
|--------------|    |--------------|
    Item3                Item4

Background code information

I have 2 layout managers; one for each layout style

private RecyclerView.LayoutManager linearLayoutManager;
private RecyclerView.LayoutManager gridLayoutManager;

In the adapter i store whether i am displaying the list as a grid or list

enum ListViewEnum{
    LIST,
    GRID
}

ListViewEnum listViewStyle = ListViewEnum.LIST;

public ListViewEnum getListStyle() {
    return listViewStyle;
}

The layout of the items in the list depend on whether the list is being rendered as a list or a grid.

ViewHolderList
ViewHolderGrid

When creating the view holder, I base the type of View Holder on the adapters list view style

protected RecyclerView.ViewHolder getItemViewHolder(ViewGroup parent){
    if (listViewStyle == ListViewEnum.LIST){
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_row, parent, false);
        return new ViewHolderList(itemView, ....);
    } else {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.grid_row, parent, false);
        return new ViewHolderGrid(itemView, ....);
    }
}

When the user clicks the actionBar button i toggle the adapter's value

getListAdapter().setListViewMode(ListViewMode.LIST);

I then refresh

listView.setLayoutManager(getListAdapter().getListStyle() == ListViewEnum.LIST ? linearLayoutManager : gridLayoutManager);
getListAdapter().notifyDataSetChanged();
listView.invalidateItemDecorations();
invalidateOptionsMenu();

I understand that i am refreshing the list adapter but it is not letting go of recycledViews. Debugging shows that on clicking the toggle button i see calls to just "onBindViewHolder" for some positions (the recycled ones) and "onCreateViewHolder" and "onBindViewHolder"(for the new ones) therefore indicating that the old view holders are being recycled.

How do i refresh the list so that it does not re-use ViewHolders from the wrong list style?

se22as
  • 2,282
  • 5
  • 32
  • 54
  • A better UX would be to create a TabbedLayout and create two RecyclerView for two fragments, <==> it doesn't obstruct your requirements – OBX Oct 18 '16 at 10:54
  • Instead of using a custom way of distinguishing between different `viewtypes` instead try the recyclerview's `getItemViewType()` I am sure while laying out the views there is a check which uses the same function call to determine if the item's viewtype has changed. Which would result in an immediate `onCreateViewHolder()` for all views. – Abbas Oct 18 '16 at 11:03
  • Abbas - brilliant, it worked perfectly and after you suggested that it makes perfect sense that using the viewtype is the correct way to go about this. Thank you so much for the suggestion and the very quick reply. – se22as Oct 18 '16 at 13:58

2 Answers2

3

The comment from @Abbas is correct, the getItemViewType() is probably your best bet. Depending on your data, you may be able to get away with one holder, but you may have to create two if the data items are different. Use the getItemViewType() call inside the adapter to determine which to show. You will either have to pass a flag into the Adapter from the activity or callback into the activity to get the type.

public static final int SMALL_VIEW = 0;
public static final int LARGE_VIEW = 1;
private int mType = 0;

@Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v;
        RecyclerView.ViewHolder viewHolder = null;

        // Create the Holders depending on the type, the view type
        // will come from the getItemViewType() call.

        switch ( viewType ) {
            case SMALL_VIEW:
                v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_row, parent, false);
                viewHolder = new ViewHolderList(mContext, v);
                break;

            case LARGE_VIEW:
                v = LayoutInflater.from(parent.getContext()).inflate(R.layout.grid_row, parent, false);

              viewHolder = new ViewHolderGrid(mContext, v);
            default:

        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        // This switch may not be necessary if you are using 
        // the same holder for both items.
        switch ( getItemViewType(position) ) {
            case SMALL_ITEM:
                ViewHolderList list = (ViewHolderList)holder;
                list.setData(mData.get(position));
                break;
            case LARGE_ITEM:
                ViewHolderGrid grid = (ViewHolderGrid)holder;
                grid.setData(mData.get(position));
        }
    }

    @Override
    public int getItemViewType(int position) {
        // Normally you identify the data type at position and
        // switch the view, for your application you can switch
        // for all.
        return mType;
    }

    public void setViewType(int type) {
       mType = type;
    }
Gary Bak
  • 4,746
  • 4
  • 22
  • 39
  • Yes i went with yours and Abbas suggestion. Unfortunately as Abbas' reply as a comment i can't mark his and yours as the solution and upvote it. I am now using the view types and it works perfectly. Thank you both so very much/ – se22as Oct 18 '16 at 13:59
1

Because of RecyclerViews Memory Management, RecyclerView tries to reuse views to scroll faster. I think using two adapter instances instead of one and re-filling the recycler with selected adapter might help.

Gary Bak
  • 4,746
  • 4
  • 22
  • 39
Mahdi Astanei
  • 1,905
  • 1
  • 17
  • 30