Bronx Bronx - 4 months ago 59
Android Question

How to use Spinner in Recyclerview?

Which are the best practice to handle a Spinner in a RecyclerView Adapter?

This is my RecyclerView Adapter:

public class CartAdapter extends BaseAdapter<Object> {

public CartAdapter(AbstractBaseActivity activity) {
super(activity);
}

public static final int TYPE_PRODOTTO = 1;
public static final int TYPE_SCONTO = 2;

@Override
public int getItemViewType(int position) {

if (items.get(position) instanceof Article)
return TYPE_PRODOTTO;
else
return TYPE_SCONTO;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View rowView = LayoutInflater.from(parent.getContext()).inflate(viewType == TYPE_PRODOTTO ? R.layout.item_cart : R.layout.item_cart_sconto, parent, false);
return new ViewHolder(rowView);
}

@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
final ViewHolder viewHolder = (ViewHolder) holder;

final Object object = items.get(position);

if (object instanceof Article) {

viewHolder.getBinding().setVariable(BR.article, object);
viewHolder.getBinding().executePendingBindings();

assert viewHolder.quantitySpinner != null;
assert viewHolder.cartoneQuantity != null;
assert viewHolder.cartoneValue != null;

CartSpinnerAdapter adapter = (CartSpinnerAdapter) viewHolder.quantitySpinner.getAdapter();
adapter.clear();
adapter.setCount(((Article) object).getQuantityAvailable());
adapter.notifyDataSetChanged();

viewHolder.quantitySpinner.setSelection(((Article) object).getQuantity() - 1); //In teoria qui la quantità non deve mai essere zero

viewHolder.cartoneQuantity.setVisibility(position % 2 == 1 ? View.GONE : View.VISIBLE); //Controllo da togliere in futuro
viewHolder.cartoneValue.setVisibility(position % 2 == 1 ? View.GONE : View.VISIBLE); //Controllo da togliere in futuro
}

final PopupMenu popup = new PopupMenu(getContext(), viewHolder.deleteMenu);
MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.delete_menu, popup.getMenu());

viewHolder.deleteMenu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
popup.show();
}
});

popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.action_delete) {
removeData(holder.getAdapterPosition());
((CartActivity) activity).checkIfEmpty();
}

return true;
}
});
}

public class ViewHolder extends RecyclerView.ViewHolder {

@BindView(R.id.item)
View item;
@Nullable
@BindView(R.id.cart_image)
ImageView cartImage;
@BindView(R.id.delete_menu)
ImageView deleteMenu;
@Nullable
@BindView(R.id.product_cartone_quantity)
TextView cartoneQuantity;
@Nullable
@BindView(R.id.product_cartone_value)
TextView cartoneValue;
@Nullable
@BindView(R.id.quantity_spinner)
AppCompatSpinner quantitySpinner;

private ViewDataBinding binding;

public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
binding = DataBindingUtil.bind(itemView);
if (quantitySpinner != null)
quantitySpinner.setAdapter(new CartSpinnerAdapter(itemView.getContext(), R.layout.support_simple_spinner_dropdown_item));
}

public ViewDataBinding getBinding() {
return binding;
}
}
}


and this is my Spinner Adapter:

public class CartSpinnerAdapter extends ArrayAdapter<String> {

LayoutInflater inflater;

int count;

public CartSpinnerAdapter(Context context, int resource) {
super(context, resource);

inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

public CartSpinnerAdapter(Context context, int resource, int count) {
super(context, resource);

this.count = count;
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

public void setCount(int count) {
this.count = count;
}

@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return getStandardView(position, parent, true);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
return getStandardView(position, parent, false);
}

@Override
public int getCount() {
return count;
}

private View getStandardView(int position, ViewGroup parent, boolean dropdown) {
View row = inflater.inflate(R.layout.support_simple_spinner_dropdown_item, parent, false);

TextView title = (TextView) row.findViewById(android.R.id.text1);

title.setText(String.valueOf(position + 1));

if (dropdown)
title.setMinWidth(Utils.dpToPx(getContext(), 64));
else
title.setAlpha(0.5f);

return row;
}
}


In this way when i scroll the RecyclerView i'm experiencing lag.

If i remove these lines everything works fine:

CartSpinnerAdapter adapter = (CartSpinnerAdapter) viewHolder.quantitySpinner.getAdapter();
adapter.clear();
adapter.setCount(((Article) object).getQuantityAvailable());
adapter.notifyDataSetChanged();


So the problem is the way i handle the adapter of the spinner, how can i handle this?

Thanks in advance.

Answer

Short

To improve performance,

  1. Remove allocations from onBindViewHolder
  2. Reuse LayoutInflater, instead of getting a new one every time.
  3. Minimize repetitive work in onBindViewHolder implementation
  4. Spinner Adapter should also recycle the views

Background

When using an adapter for scrolling, the most important thing to make sure is that we DON'T allocate new objects (or minimize it as possible).

The whole purpose of a RecyclerView with an Adapter is to make sure we Recycle our objects so that the work needed during scroll is minimal.

Since allocating memory is very "expensive", to improve scrolling performance, the first thing to look for is allocations during the onBindViewHolder. All allocations if any should be made in the onCreateViewHolder.

Once all allocations are cleared, if we still have lagging, it is time for some micro improvements. These includes improving code quality, reuse logic results, etc.

What to do?

1) Remove allocations from onBindViewHolder

In the following code:

final PopupMenu popup = new PopupMenu(getContext(), viewHolder.deleteMenu);
MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.delete_menu, popup.getMenu());

viewHolder.deleteMenu.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        popup.show();
    }
});

popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem item) {
        if (item.getItemId() == R.id.action_delete) {
            removeData(holder.getAdapterPosition());
            ((CartActivity) activity).checkIfEmpty();
        }

        return true;
    }
});

You currently have 3 direct allocations (new) and some indirect allocations (inflate). Change this code so that all allocations are in the onCreateViewHolder. For example:

In onCreateViewHolder do the allocations like so:

// Allocate Listener only ONCE per recycled view 
viewHolder.deleteMenu.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // Get needed data from the view TAG, we will set it later
        final int itemPosition = (Integer)view.getTag();

        // Do work only when needed - when user clicked the button
        final PopupMenu popup = new PopupMenu(getContext(), viewHolder.deleteMenu);
        MenuInflater inflater = popup.getMenuInflater();
        inflater.inflate(R.menu.delete_menu, popup.getMenu());

        popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                // Do logic using itemPosition etc
                return true;
            }
        });

        popup.show();
    }
});

In onBindViewHolder bind relevant data like so:

viewHolder.deleteMenu.setTag(holder.getAdapterPosition());

2) Reuse LayoutInflater, instead of getting a new one every time.

In the following code:

public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View rowView = LayoutInflater.from(parent.getContext()).inflate(viewType == TYPE_PRODOTTO ? R.layout.item_cart : R.layout.item_cart_sconto, parent, false);
    return new ViewHolder(rowView);
}

You are getting a new LayoutInflater every time. It is a waste. Better to get one in the Adapter constructor and save it as a member.

3) Minimize repetitive work in onBindViewHolder implementation

For example, in the following code:

viewHolder.cartoneQuantity.setVisibility(position % 2 == 1 ? View.GONE : View.VISIBLE); //Controllo da togliere in futuro
viewHolder.cartoneValue.setVisibility(position % 2 == 1 ? View.GONE : View.VISIBLE); //Controllo da togliere in futuro

You are calculating the same logic twice. Better to calculate it once and reuse the outcome:

int cartoneVisibility = position % 2 == 1 ? View.GONE : View.VISIBLE;
viewHolder.cartoneQuantity.setVisibility(cartoneVisibility); //Controllo da togliere in futuro
viewHolder.cartoneValue.setVisibility(cartoneVisibility); //Controllo da togliere in futuro

4)Spinner Adapter should also recycle the views

In CartSpinnerAdapter.getView() you are also allocating memory. It happens (every time * list item * count) - That is a lot of allocations. Please use the convertView instead. Have a look on this tutorial dzone.com/articles/android-listview-optimizations

Comments