Reputation: 17132
I have a RecyclerView
which I want to be populated with String
objects sometimes & with Product
objects some other time. So I started creating its manager adapter this way:
// BaseSearchAdapter is the class that contains the 'List<T> mItems' member variable
public class SearchAdapter<T> extends BaseSearchAdapter<SearchAdapter.ViewHolder, T> {
private Context mContext;
public SearchAdapter(Context context, List<T> items) {
mContext = context;
mItems = new ArrayList<>(items);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
T item = mItems.get(position);
holder.bind(item);
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView textLabel;
public ViewHolder(View v) {
}
public void bind(T item) {
textLabel.setText(...); // How to handle T ?
}
}
}
where T
could be String
or Product
according to the plan.
My question is how can I appropriately bind the data (whether it's a String
or a Product
) object to its corresponding view in this situation? Or is there a better way to handle this ?
Upvotes: 4
Views: 8053
Reputation: 3711
What I do on my projects is create a class BaseRecyclerAdapter
that has all of my common operations. Then for most adapters all I have to define is the ViewHolder and the layout.
UPDATE As Requested I posted a fuller version of my BaseRecyclerAdapter (it varies a bit based upon project need). Also include is a simple gesture callback that allows you to easily enable swipe to remove or drag to reorder operations.
NOTE: This version updates how the recycler item layouts are inflated. I now prefer to inflate the in the BaseRecyclerAdapter.ViewHolder constructor allowing the layout to be specified in the extending ViewHolder's constructor.
Example Base Adapter
public abstract class BaseRecyclerAdapter<T> extends RecyclerView.Adapter<BaseRecyclerAdapter.ViewHolder> {
private final List<T> items = new ArrayList<>();
OnItemSelectedListener<T> onItemSelectedListener = SmartNull.create(OnItemSelectedListener.class);
public BaseRecyclerAdapter setOnItemSelectedListener(OnItemSelectedListener<T> onItemSelectedListener) {
if (onItemSelectedListener == null) {
this.onItemSelectedListener = SmartNull.create(OnItemSelectedListener.class);
} else {
this.onItemSelectedListener = onItemSelectedListener;
}
return this;
}
public boolean isEmpty() {
return items.isEmpty();
}
public void setItems(List<T> items) {
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
}
public void addItems(T... items) {
addItems(Arrays.asList(items));
}
public void addItems(List<T> items) {
int startPosition = this.items.size() - 1;
this.items.addAll(items);
notifyItemRangeInserted(startPosition, items.size());
}
public void removeItem(int position) {
T item = items.remove(position);
if (itemRemovedListener != null) {
itemRemovedListener.onItemRemoved(item);
}
notifyItemRemoved(position);
}
public void removeItem(T t) {
int index = items.indexOf(t);
if (index >= 0) {
removeItem(index);
}
}
public void addItem(T item) {
items.add(item);
notifyItemInserted(getItemCount() - 1);
}
public void moveItem(int startPosition, int targetPosition) {
if (startPosition < targetPosition) {
for (int i = startPosition; i < targetPosition; i++) {
Collections.swap(items, i, i + 1);
}
} else {
for (int i = startPosition; i > targetPosition; i--) {
Collections.swap(items, i, i - 1);
}
}
notifyItemMoved(startPosition, targetPosition);
}
public List<T> getItems() {
return new ArrayList<>(items);
}
public void setItemAt(int position, T item){
items.set(position, item);
notifyItemChanged(position);
}
public void refreshItem(T item) {
int i = items.indexOf(item);
if (i >= 0) {
notifyItemChanged(i);
}
}
protected void setItemWithoutUpdate(int position, T item){
items.set(position, item);
}
public int indexOf(T t) {
return items.indexOf(t);
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bindItemAt(getItemAt(position), position);
}
@Override
public int getItemCount() {
return items.size();
}
public T getItemAt(int position) {
return items.get(position);
}
private void onItemSelected(int position) {
if (isValidPosition(position)) {
onItemSelectedListener.onItemSelected(getItemAt(position));
}
}
boolean isValidPosition(int position) {
return position >=0 && position < items.size();
}
public abstract class ViewHolder<T> extends RecyclerView.ViewHolder implements View.OnClickListener {
public ViewHolder(ViewGroup parent, @LayoutRes int layoutId) {
super(LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false));
itemView.setOnClickListener(this);
}
public ViewHolder(View view) {
super(view);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
onItemSelected(getAdapterPosition());
}
public abstract void bindItemAt(T t, int position);
}
public interface OnItemSelectedListener<T> {
void onItemSelected(T t);
}
}
Example Implementation
public class ExampleAdapter extends com.stratospherequality.mobileworkforce.modules.common.BaseRecyclerAdapter<Long> {
@Override
protected RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(parent);
}
private class ViewHolder extends RecyclerViewHolder<Long> {
TextView text;
public ViewHolder(ViewGroup parent) {
super(parent, R.layout.row_my_layout);
// I typically use ButterKnife here but this works as well
text = (TextView) itemView.findViewById(R.id.text);
}
@Override
public void setItem(Long value, int position) {
text.setText("#" + value);
}
}
}
Base GestureCallback
public class AdapterGestureCallback extends ItemTouchHelper.SimpleCallback {
public interface OnRemoveItemCallback<T> {
void onRemoveItem(BaseRecyclerAdapter<T> adapter, T t);
}
public enum Direction {
UP(ItemTouchHelper.UP),
DOWN(ItemTouchHelper.DOWN),
LEFT(ItemTouchHelper.LEFT),
RIGHT(ItemTouchHelper.RIGHT),
START(ItemTouchHelper.START),
END(ItemTouchHelper.END);
public final int value;
Direction(int value) {
this.value = value;
}
}
private final BaseRecyclerAdapter adapter;
private OnRemoveItemCallback onRemoveItemCallback;
private boolean enabled = true;
public AdapterGestureCallback(BaseRecyclerAdapter adapter) {
super(0, 0);
this.adapter = adapter;
}
public AdapterGestureCallback setOnRemoveItemCallback(OnRemoveItemCallback onRemoveItemCallback) {
this.onRemoveItemCallback = onRemoveItemCallback;
return this;
}
public AdapterGestureCallback setEnabled(boolean enabled) {
this.enabled = enabled;
return this;
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
adapter.moveItem(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
if (onRemoveItemCallback == null){
adapter.removeItem(position);
} else {
onRemoveItemCallback.onRemoveItem(adapter, adapter.getItemAt(position));
}
}
public AdapterGestureCallback withDragDirections(Direction... dragDirections) {
setDefaultDragDirs(valueFor(dragDirections));
return this;
}
public AdapterGestureCallback withSwipeDirections(Direction... swipeDirections) {
setDefaultSwipeDirs(valueFor(swipeDirections));
return this;
}
@Override
public boolean isItemViewSwipeEnabled() {
return enabled;
}
@Override
public boolean isLongPressDragEnabled() {
return enabled;
}
public int valueFor(Direction... directions) {
int val = 0;
for (Direction d : directions) {
val |= d.value;
}
return val;
}
public AdapterGestureCallback attach(RecyclerView recyclerView) {
new ItemTouchHelper(this).attachToRecyclerView(recyclerView);
return this;
}
}
GestureCallback with swiping
new AdapterGestureCallback(adapter)
.withSwipeDirections(AdapterGestureCallback.Direction.LEFT, AdapterGestureCallback.Direction.RIGHT)
.setOnRemoveItemCallback(this)
.attach(recyclerView);
Upvotes: 5
Reputation: 17131
Can use generic for Holder like this:
public abstract class ActionBarAdapter<T,VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private Context mContext;
public SearchAdapter(Context context, List<T> items) {
mContext = context;
mItems = new ArrayList<>(items);
}
}
Upvotes: 1
Reputation: 81539
// BaseSearchAdapter is the class that contains the 'List<T> mItems' member variable
public class SearchAdapter<T> extends BaseSearchAdapter<SearchAdapter.ViewHolder<T>, T> {
private Context mContext;
private ViewHolderBinder<T> mBinder;
public SearchAdapter(Context context, List<T> items, ViewHolderBinder<T> binder) {
mContext = context;
mItems = new ArrayList<>(items);
mBinder = binder;
}
@Override
public void onBindViewHolder(ViewHolder<T> holder, int position) {
T item = mItems.get(position);
holder.bind(item);
}
public static class ViewHolder<T> extends RecyclerView.ViewHolder {
ViewHolderBinder<T> mBinder;
TextView textLabel;
public ViewHolder(View v, ViewHolderBinder<T> binder) {
textLabel = (TextView)v.findViewById(R.id.text_label);
this.mBinder = binder;
}
public void bind(T item) {
binder.bind(this, item);
}
}
public interface ViewHolderBinder<T> {
void bind(ViewHolder<T> viewHolder, T item);
}
public static class StringViewHolderBinder implements ViewHolderBinder<String> {
@Override
public void bind(ViewHolder<String> viewHolder, String item) {
viewHolder.textLabel.setText(item);
}
}
public static class ProductViewHolderBinder implements ViewHolderBinder<Product> {
@Override
public void bind(ViewHolder<Product> viewHolder, Product item) {
viewHolder.textLabel.setText(item.getName());
}
}
}
Upvotes: 6
Reputation: 40193
The approach that, for example, ArrayAdapter<T>
uses is to call toString()
on whatever T
you pass. This will of course work for a String
, and you'll have to implement toString()
in your Product
to return a meaningful representation.
Upvotes: 0