Making a Multiple View Types Adapter With Annotations

This post is more an introduction to java annotations than a tutorial about how to create an adapter able to show different rows. I won’t describe how to implement a custom Adapter. Indeed, ListViews and Adapters are common things in Android and you can easily find a tutorial about such components.

TLDR: I made a GIST.

Delegates

Today I faced a problem. I needed a ListView able to display many different types of row. This is the common implementation of the getView method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public int getViewTypeCount() {
  return fNUM_TYPES;
}
  

public View getView(etc...){
  if(convertView == null){
      switch(getItemViewType(position)){
          //ugly things happen here…
          case 0: //create view 0 + viewHolder0 and other stuff
              break;
          case 1: //create view 1 + viewHolder1 and other stuff
              break;
          case 2: //create view 2 + viewHolder2 and other stuff
              break;
          case 3: //ok you get it

This implementation works well if you have max 2 types of rows (ok, you will have 2 in most cases, with sections and items), but if you have more types, your getView(…) is going to be hard to read and maintain.

The first idea is to create custom classes in which we can delegate the getView.

Create an interface called DelegateAdapter:

1
2
3
public interface DelegateAdapter {
  public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, Object item);
}

Then you have to create a custom class implementing DelegateAdapter for each type you can have in your list. For example:

1
2
3
4
5
6
7
8
public class SimpleTextDelegateAdapter implements DelegateAdapter {

  @Override
  public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, Object item) {
      if(convertView == null){
          //same as always… create your view, set a viewholder, etc.
      }
  }

Then you can call the right delegate on your getView():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public int getViewTypeCount() {
  return mDelegateAdapterSparseArray.size();
}
  

public View getView(...){
  switch(getItemViewType(position)){
      case 0: return mDelegate0.getView()
          break;
      case 1: return mDelegate1.getView()
          break;
      case 2: return mDelegate2.getView()
          break;
      case 3: return mDelegate3.getView()

Ok that’s better but we still have our switch case… Let’s create a LongSparseArray (what is this?) to get rid of this switch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private   LongSparseArray<DelegateAdapter> mDelegateAdapterSparseArray;

/**
 * Constructor
 */
public MyCustomAdapter(...){
  //some initializations…
  initDelegates();
}

private void initDelegates(){
  mDelegateAdapterSparseArray = new LongSparseArray<DelegateAdapter>();
  
  //the first parameter represents the ItemViewType
  mDelegateAdapterSparseArray.put(0, new SimpleTextDelegateAdapter());
  mDelegateAdapterSparseArray.put(1, new HtmlTextDelegateAdapter());
  mDelegateAdapterSparseArray.put(2, new GalleryDelegateAdapter());
  //etc.
}

public View getView(...){
  DelegateAdapter adapter = mDelegateAdapterSparseArray.get(getItemViewType(position));
  if(adapter != null){
      convertView = adapter.getView();
  }else{
      //default case! this should not happen :p
  }
  return convertView;
}

Ok we could stop there, but I never created java annotations and I’m very curious… so I found a way to use it in that case.

Annotations

Annotations are created with the @interface keyword. For each annotation you can define some parameters:

  • @Target can specify the place your annotation can be used (as a class header, method header, etc.).

  • @Retention specify how long your annotation will live. It can be SOURCE, CLASS or RUNTIME.

  • SOURCE: The annotations are not saved in *.class files.
  • CLASS: The annotations are saved in *.class files, but can’t be used by the VM. (default parameter)
  • RUNTIME: Annotations save in *.class and can be used in runtime. Since we will use reflection, we need this parameter.

Please note that I’m showing you one way to do this but there are plenty.

We will need 2 different annotations:

  • @DelegateAdapters: used on our BaseAdapter to indicate which adapters to delegate the getView. It will take the delegate classes as parameter.

  • @DelegateAdapterType: used on our DelegateAdapters implementations. This annotation take as parameter his itemViewType associated.

DelegateAdapters Annotation:

1
2
3
4
5
@Target(ElementType.TYPE)    
@Retention(RetentionPolicy.RUNTIME)
public @interface DelegateAdapters {
  Class<? extends DelegateAdapter>[] delegateAdapters();
}

DelegateAdapterType Annotation:

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DelegateAdapterType {
  long itemType();
}

Note: You can set the itemType as optional if you set a default value:

1
long itemType() default 0;

We will use our annotations like that:

In our base adapter:

1
2
3
4
5
6
7
8
@DelegateAdapters(delegateAdapters = {
      SimpleTextDelegateAdapter.class,
      HtmlTextDelegateAdapter.class,
      GalleryDelegateAdapter.class
})
public class MyCustomAdapter extends BaseAdapter{
  
}

And in our delegates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@DelegateAdapterType(itemType = 0) //note that you can use constants
public class SimpleTextDelegateAdapter implements DelegateAdapter {
  
}

@DelegateAdapterType(itemType = 1) //note that you can use constants
public class HtmlTextDelegateAdapter implements DelegateAdapter {
  
}

@DelegateAdapterType(itemType = 2) //note that you can use constants
public class GalleryDelegateAdapter implements DelegateAdapter {
  
}

Now our adapters are ready, we just need to initialize the SparseArray using reflection in the Base Adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void initDelegates(){
  mDelegateAdapterSparseArray = new LongSparseArray<DelegateAdapter>();
  
  //get the annotation containing all the delegate classes
  DelegateAdapters annotation = getClass().getAnnotation(DelegateAdapters.class);
  if (annotation != null) {
      Class[] clazzs = annotation.delegateAdapters();
      for (Class<?> clazz : clazzs) {
          DelegateAdapterType delegateAdapterAnnotation = clazz.getAnnotation(DelegateAdapterType.class);
          //check if each delegate has an itemType
          if(delegateAdapterAnnotation == null){
              throw new RuntimeException("The class "+clazz.getName()+" should have the annotation DelegateAdapterType");
          }
          
          long itemtype = delegateAdapterAnnotation.itemType();
          if(mDelegateAdapterSparseArray.get(itemtype) != null){
              throw new RuntimeException("The item type "+itemtype+" is already defined!");
          }
          
          //instantiate with the default constructor
          DelegateAdapter adapter = null;
          try {
              adapter = (DelegateAdapter) clazz.newInstance();
          } catch (Exception e) {
              throw new RuntimeException("Error while instantiating "+clazz.getName()+" with default constructor: "+e.getMessage(), e);
          }

          //final step!
          mDelegateAdapterSparseArray.put(itemtype, adapter);
      }
  }
  
}

Conclusion

I hope you found it interesting. Please note this is my first custom annotation, so don’t hesitate to tell me (via comments / g+ / twitter, etc.) if you have any remark.

TLDR: I made a GIST. Check it out and don’t hesitate to modify it!

Comments