3
\$\begingroup\$

Like everyone, I use ListViews 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);
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

It looks good, few comments though:

  • OOP is all about implementation hiding, so make sure you hide as much as possible.By default, instance variables are package private, which is the case for your variables.

    Activity ctx;
    ArrayList<ModelType> array = new ArrayList<ModelType>();
    Constructor<?> viewConstructor;
    

    I would rather hide them to the maximum by declaring them private.

    private Activity ctx;
    private ArrayList<ModelType> array = new ArrayList<ModelType>();
    private Constructor<?> viewConstructor;
    
  • Information about Activity is available on construction time, and thus I would declare it final

    private final Activity ctx;
    

    -If you overriding a method, make you sure you annotate it by @Override

    @Override
    public int getCount(){
     //..
    }
    
  • Do checks on your array boundaries,

    public Object getItem(int position) {
     if(position < 0 || position >= size())
       throw new ArrayIndexOutOfBoundsException();// or something more specific to your API
      return array.get(position);
    }
    
\$\endgroup\$
2
  • \$\begingroup\$ Agreed. I forget the private frequently, its the C++ coder in me. I forget that private isn't the default. The getCount method is defined by the Adapter class (the grandparent class through BaseAdapter)- I'm overriding that function and the name can't be changed. \$\endgroup\$ Jun 30, 2014 at 23:15
  • \$\begingroup\$ @GabeSechan no problem, I didnt know that you it's overridden,and do annotate your methods by '@Override' when you override them \$\endgroup\$ Jun 30, 2014 at 23:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.