In my last post, I described some of the problems you may experience with ArrayAdapter in situations involving filtering. I also promised an alternative implementation tailored for those scenarios, which brings us to this post today.

As always, all the source code can be found in the blogActivity repository. If you take a look at it and come up with improvements or fixes, please contact me!

vs. ArrayAdapter

FilterableAdapter is an abstract class that extends BaseAdapter and implements Filterable. It’s meant to address the following issues with ArrayAdapter.

ArrayAdapter FilterableAdapter
Confusing contract. Different behavior with and without filtering. Construction from arrays can be a problem. Consistent behavior. Works exactly the same with and without filtering, and is properly initialized both from collections and plain arrays.
Misleading behavior of notifyDataSetChanged(). Reverts to setNotifyOnChange(true) behind your back. Needs notifyDataSetChanged(), unless and only unless modifications are done through the Adapter and setNotifyOnChange(true) was applied.
Hard to extend for custom filtering. Overriding getFilter() is not very helpful, since most of the other members are private. Easily customizable filtering. Little to no additional code other than the filtering logic.

It’s not meant to always replace ArrayAdapter. You should still use one if you don’t need filtering and your backing data is extensive, since FilterableAdapter does some extra book-keeping to keep its behavior consistent, and it may not be really necessary for unfiltered lists.

Using FilterableAdapter

FilterableAdapter <ObjectType, ConstraintType> takes two generic parameters: the type of objects in the backing data set, and the type of the constraint object used for filtering. It takes care of all the filtering and syncing procedures. You only have to define its filtering criterion, by way of overriding two methods:

  1. prepareFilter(): this method takes the CharSequence passed to filter() and generates a ConstraintType object that will (predictably) define the constraint.
  2. passesFilter(): this method receives both the object returned by prepareFilter() and an object contained in the adapter’s data set, and returns a boolean. True means the object passes the filter and should be displayed, false means otherwise.

Let’s see, for example, how to extend FilterableAdapter to implement the default filtering behavior of ArrayAdapter.

public class SimpleFilterableAdapter<ObjectType> extends FilterableAdapter<ObjectType, String> {
    // (...inherited constructors...)

    @Override
    protected String prepareFilter(CharSequence seq) {

        /* The object we return here will be passed to passesFilter() as constraint.
        ** This method is called only once per filter run. The same constraint is
        ** then used to decide upon all objects in the data set.
        */

        return seq.toString().toLowerCase();
    }

    @Override
    protected boolean passesFilter(ObjectType object, String constraint) {
        String repr = object.toString().toLowerCase();
        
        if (repr.startsWith(constraint))
            return true;
        
        else {
            final String[] words = repr.split("");
            final int wordCount = words.length;
            
            for (int i = 0; i < wordCount; i++) {
                if (words[i].startsWith(constraint))
                    return true;
            }
        }
        
        return false;
    }
}

Using this Adapter in your code is just as easy as using an ArrayAdapter:

public class SampleActivity extends ListActivity {

    private SimpleFilterableAdapter<String> mAdapter;
    private List<String> mObjects = new ArrayList<String>();
       
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.enhanced_array_adapter_test);
        
        getListView().setAdapter(mAdapter = new SimpleFilterableAdapter<String>(
            this,
            android.R.layout.simple_list_item_1,
            mObjects)
        );
        
        mObjects.addAll(/* some elements */);
        mAdapter.notifyDataSetChanged();
    }
}

Final thoughts

Even though it is, in my opinion, consistent and easy to use and configure, I’m not completely satisfied with FilterableAdapter. There’s one last thing bugging me.

Giving the user of the class the ability to provide an external collection to use as data set means that the underlying list may suffer modifications without the Adapter’s knowledge. As a consequence, a lot of extra refiltering has to be done whenever the Adapter is notifiedDataSetChanged().

Most of the time, however, you let the Adapter handle the data set, and don’t ever bother having an external reference to it. If this was mandatory, and all modifications needed to go through the Adapter, the process of notifying changes and reapplying filters would be much more efficient.

I kept this behavior because I wanted FilterableAdapter to mimic the features of ArrayAdapter as much as possible. I may come up with another alternative in the next few days.

Advertisements