Friday 26 October 2012

Generic adapter for all ListView in android

When using a custom list adapter, you have to do the same work: load row xml, get view by id, map logic data to view, then set adapter for ListView. Some thing like this:
public class CommonAdapter extends BaseAdapter {
 private Context context;
 private List lst;
 int layoutID;

 public CommonAdapter(Context context, List lstData, int layoutID) {
  this.context = context;
  this.lst = lstData;
  this.layoutID = layoutID;
 }

 public static class ViewHolder {
  public TextView Text;
  public TextView SubText;
 }

 public View getView(int position, View convertView, ViewGroup parent) {
  View v = convertView;
  ViewHolder viewHolder;
  if (v == null) {
   LayoutInflater inflater = (LayoutInflater) context
           .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   v = inflater.inflate(layoutID, parent, false);
   viewHolder = new ViewHolder();
   viewHolder.Text = (TextView) v.findViewById(R.id.lstmText);
   viewHolder.SubText = (TextView) v.findViewById(R.id.lstmSub);
   v.setTag(viewHolder);
  } else {
   viewHolder = (ViewHolder) v.getTag();
  }
  viewHolder.Text.setText(lst.get(position).Text);
  viewHolder.SubText.setText(lst.get(position).SubText);

  Static.setiFont(viewHolder.Text);
  Static.setiFont(viewHolder.SubText);
  return v;
 }

 public int getCount() {
  return lst.size();
 }

 public Object getItem(int position) {
  return lst.get(position);
 }

 public long getItemId(int position) {
  return position;
 }
 
 public class CommonAdapterData {
  public int id;
  public String Text;
  public String SubText;
 }

}

For many kinds of list view 's row (difference template), we have to create an suitable adapter.
Now, we have another way which can make your work is easy.
First, we need the interface define the way to build row view.
public interface Adaptable {
 public View buildView(View v, LayoutInflater inflater, ViewGroup parent);
}

Then, we create a Generic adapter to use this interface above. There is no magic.
public class GenericAdapter extends BaseAdapter {
 private LayoutInflater inflater;
 private List items;

 @SuppressWarnings("unchecked")
 public GenericAdapter(List items, Context c) {
  this.items = (List) items;
  inflater = LayoutInflater.from(c);
 }

 @Override
 public int getCount() {  
  return items.size();
 }

 @Override
 public Object getItem(int position) {
  return items.get(position);
 }

 @Override
 public long getItemId(int position) {
  return position;
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  return items.get(position).buildView(convertView, inflater, parent); }

}

After that, we create an view holder which holder the control in row view (TextView, ImageView, EditText...) and we will map it with an id by using annotation.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InvokeView {
 int viewId();
}
Assume we have an entity:
public class Mobile {
 public String name;
 public int image;
 
 public Mobile(){}
 
 public Mobile(String name, int image){
  this.name = name;
  this.image = image;
 }
}
Now we create an BaseView abstract class, which can be load row xml layout, map view id to viewhoder by using reflect.
public abstract class BaseView implements Adaptable{
 private static final String TAG = "BaseView";
 protected int layoutId;
 protected T viewHolder;
 protected E entity;
 public BaseView(){
  
 }
 
 public BaseView(E entity, int layoutId){
  this.entity = entity;
  this.layoutId = layoutId;
 }
 protected void invokeView(View v){
  try {
   Field fs[] = viewHolder.getClass().getFields();
   for (Field f : fs) {

      InvokeView a = (InvokeView)
                              f.getAnnotation(InvokeView.class);
      int id = a.viewId();
      Log.d(TAG, "field name: " + f.getName());
      Log.d(TAG, "view id: " + id);
      Log.d(TAG, "class: " + f.getClass());
      f.set(viewHolder, v.findViewById(id));

   }
  } catch (Exception ex) {
   ex.printStackTrace();
  }
 }
 
 @SuppressWarnings("unchecked")
 @Override
 public View buildView(View v, LayoutInflater inflater, ViewGroup parent) {
  // load the view
  if (null == v) {
   v = inflater.inflate(layoutId, parent, false);
   // get the view
   invokeView(v);
   v.setTag(viewHolder);
  } else {
   viewHolder = (T) v.getTag();
  }

  // binding logic data to view
  mappingData(viewHolder, entity);

  return v;
 }
 
 protected abstract void mappingData(T viewHolder, E entity);
}
Then we define how to map data from entity to the view through view holder. This is a sub class of BaseView.
public class MobileView extends BaseView {
 public MobileView(Mobile mobile, int layoutId) {
  super(mobile, layoutId);
  this.viewHolder = new MobileViewHolder();
 }

 @Override
 public void mappingData(MobileViewHolder viewHolder, Mobile entity) {
  viewHolder.text.setText(entity.name);
  viewHolder.image.setBackgroundResource(entity.image);
 }

}
Finally, we can use our adapter for list view.
public class ListMobileActivity extends ListActivity {

 private static final int FRUIT_ID = 0;
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  List lst = new ArrayList();
  for(int i = 0; i < 10; i++){
   MobileView mv = new MobileView(new Mobile(String.valueOf(i), R.drawable.android_logo), R.layout.list_mobile);
   lst.add(mv);
  }
  setListAdapter(new GenericAdapter(lst, getApplicationContext()));
 }
}
Also, I give an source code, which can be found here

2 comments:

  1. I'm new to both Java and Android, but this same issue of a generic approach to the reuseability of listviews had also occurred to me

    - thanks for posting, now I just have to get my head around it!

    ReplyDelete
  2. Thanks, this is great!. However when tried to implement it in AlertDialog, one of the list item keeps changing when scrolled down/up. From the code, I intended to increase the loop from 10 to 60 before adding the "mv" from the list so we can scroll from the dialog.

    The code below were added from the onCreate() of ListMobileActivity:

    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Alert Dialog Title");
    final GenericAdapter mGenericAdapter = new GenericAdapter(lst, this);
    builder.setAdapter(mGenericAdapter, new DialogInterface.OnClickListener() {

    @Override
    public void onClick(DialogInterface arg0, int selectedIndex) {
    // Do nothing
    }
    });
    AlertDialog alert = builder.create();
    alert.show();


    I wonder if there is workaround on this...

    ReplyDelete