Like everyone, I use ListView
s in a lot of apps. And I pretty much always end up making a custom Adapter. Yet 90% of my custom adapters end up doing the same thing (mapping directly from an ArrayList
or array) with getView()
overridden to update the views. I hate rewriting this part of the code. I also hate that the code itself violates MVC so badly- I want the view classes to directly read from the model, not have my control classes make dozens of UI calls.
This code is a solution to that. Its a generic array adapter that can work on any model class, and calls a setModel()
function on a view class that can be overridden to display different types of models.
It does have 2 weaknesses. One is that I was forced to rely on reflection to avoid a weakness in Java generics- you cannot call a generic constructor due to type erasure. A factory interface could have been used instead, but since either is ugly I went with the way that requires less code in the clients. The other is that it adds 1 extra ViewGroup
per row of the ListView
, where a custom implementation of this same pattern can avoid that, but I'm ok with that.
I guess I'm looking for opinions on the pattern and utility. If you have ideas on how to get rid of the reflection that doesn't involve a factory class for each adapter instance I'm all ears.
This code has been tested for simple use cases, but not thoroughly. More complex ones may fail, if so I apologize.
Edit: I've changed the classes around a bit and made it so the id of the layout is not part of the adapter at all, and the view should know it. This simplifies thing a bit.
This is the adapter class:
public class MVCArrayAdapter<ModelType> extends BaseAdapter{
Activity ctx;
ArrayList<ModelType> array = new ArrayList<ModelType>();
Constructor<?> viewConstructor;
public MVCArrayAdapter(Activity context, String viewClassName) throws NoSuchMethodException, ClassNotFoundException {
super();
ctx = context;
viewConstructor = Class.forName(viewClassName).getConstructor(Activity.class);
}
public int getCount() {
return array.size();
}
public Object getItem(int position) {
return array.get(position);
}
public long getItemId(int position) {
return position;
}
@SuppressWarnings("unchecked")
public View getView(int position, View convertView, ViewGroup parent) {
ListViewRow<ModelType> view = (ListViewRow<ModelType>)convertView;
if (view == null) {
try {
view = (ListViewRow<ModelType>)viewConstructor.newInstance(ctx);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
view.setModel((ModelType)getItem(position));
return view;
}
public void add(ModelType object){
array.add(object);
}
public void addAll(Collection<? extends ModelType> objects){
array.addAll(objects);
}
public void addAll(ModelType... objects){
for(ModelType object : objects){
array.add(object);
}
}
public void clear(){
array.clear();
}
public void insert(ModelType object, int index){
array.set(index, object);
}
public void remove(ModelType object){
array.remove(object);
}
public void sort(Comparator<? super ModelType> comparator){
Collections.sort(array, comparator);
}
}
And the row view class:
public abstract class ListViewRow<ModelType> extends FrameLayout {
ModelType model;
View childView;
public ListViewRow(Activity context, int viewLayout) {
super(context);
LayoutInflater inflater = context.getLayoutInflater( );
childView = inflater.inflate( viewLayout, this, false );
addView(childView);
}
public abstract void setModel(ModelType newModel);
}
Here's an example child view class that sets a text field and a image field, to show the amount of work in creating the row view:
public class AdapterRow extends ListViewRow<String> {
TextView tv;
ImageView iv;
public AdapterRow(Activity context) {
super(context, R.layout.li);
tv = (TextView)findViewById(R.id.tv);
iv = (ImageView)findViewById(R.id.iv);
}
public void setModel(String str){
tv.setText(str);
iv.setImageResource(R.drawable.ic_launcher);
}
}