Question

I have a ListView (with setTextFilterEnabled(true)) and a custom adapter (extends ArrayAdapter) which I update from the main UI thread whenever a new item is added/inserted. Everything works fine at first--new items show up in the list immediately. However this stops the moment I try to filter the list.

Filtering works, but I do it once and all of my succeeding attempts to modify the contents of the list (add, remove) don't display anymore. I used the Log to see if the adapter's list data gets updated properly, and it does, but it's no longer in sync with the ListView shown.

Any ideas what's causing this and how best to address the issue?

Was it helpful?

Solution

I went through ArrayAdapter's actual source code and it looks like it was actually written to behave that way.

ArrayAdapter has two Lists to begin with: mObjects and mOriginalValues. mObjects is the primary data set that the adapter will work with. Taking the add() function, for example:

public void add(T object) {
    if (mOriginalValues != null) {
        synchronized (mLock) {
            mOriginalValues.add(object);
            if (mNotifyOnChange) notifyDataSetChanged();
        }
    } else {
        mObjects.add(object);
        if (mNotifyOnChange) notifyDataSetChanged();
    }
}

mOriginalValues is initially null so all operations (add, insert, remove, clear) are by default targeted to mObjects. This is all fine until the moment you decide to enable filtering on the list and actually perform one. Filtering for the first time initializes mOriginalValues with whatever mObjects has:

private class ArrayFilter extends Filter {
    @Override
    protected FilterResults performFiltering(CharSequence prefix) {
        FilterResults results = new FilterResults();

        if (mOriginalValues == null) {
            synchronized (mLock) {
                mOriginalValues = new ArrayList<T>(mObjects);
                //mOriginalValues is no longer null
            }
        }

        if (prefix == null || prefix.length() == 0) {
            synchronized (mLock) {
                ArrayList<T> list = new ArrayList<T>(mOriginalValues);
                results.values = list;
                results.count = list.size();
            }
        } else {
            //filtering work happens here and a new filtered set is stored in newValues
            results.values = newValues;
            results.count = newValues.size();
        }

        return results;
    }

    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        //noinspection unchecked
        mObjects = (List<T>) results.values;
        if (results.count > 0) {
            notifyDataSetChanged();
        } else {
            notifyDataSetInvalidated();
        }
    }
}

mOriginalValues now has a copy of, well, the original values/items, so the adapter could do its work and display a filtered list thru mObjects without losing the pre-filtered data.

Now forgive me (and please do tell and explain) if my thinking is incorrect but I find this weird because now that mOriginalValues is no longer null, all subsequent calls to any of the adapter operations will only modify mOriginalValues. However since the adapter was set up to look at mObjects as its primary data set, it would appear on screen that nothing is happening. That is until you perform another round of filtering. Removing the filter triggers this:

if (prefix == null || prefix.length() == 0) {
            synchronized (mLock) {
                ArrayList<T> list = new ArrayList<T>(mOriginalValues);
                results.values = list;
                results.count = list.size();
            }
        }

mOriginalValues, which we've been modifying since our first filter (although we couldn't see it happening on screen) is stored in another list and copied over to mObjects, finally displaying the changes made. Nevertheless it will be like this from this point onwards: all operations will be done on mOriginalValues, and changes will only appear after filtering.

As for the solution, what I've come up with at the moment is either (1) to put a boolean flag which tells the adapter operations if there is an ongoing filtering or not--if filtering is complete, then copy over the contents of mOriginalValues to mObjects, or (2) to simply call the adapter's Filter object and pass an empty string *.getFilter().filter("") to force a filter after every operation [as also suggested by BennySkogberg].

It would be highly appreciated if anybody can shed some more light on this issue or confirm what I just did. Thank you!

OTHER TIPS

If I understand your problem well - do you call the notifyDataSetChanged() method? It forces the listview to redraw itself.

listAdapter.notifyDataSetChanged();

just use this whenever you do something (for instance: lazy-loading of images) to update the listview.

The problem is reported as a defect since July 2010. Check out my comment #5

First of all I would like to mention, that @jhie's answer is very good. However those facts did not really solve my problem. But with the help of knowledge of how the internally used mechanism worked, I could write it my own filtering function:

public ArrayList<String> listArray = new ArrayList<>();
public ArrayList<String> tempListArray = new ArrayList<>();
public ArrayAdapter<String> tempAdapter;
public String currentFilter;

in onCreate:

// Fill my ORIGINAL listArray;
listArray.add("Cookies are delicious.");

// After adding everything to listArray, I add everything to tempListArray
for (String element : listArray){
    tempListArray.add(element);
}

// Setting up the Adapter...
tempAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, tempListArray);
myListView.setAdapter(tempAdapter);

Filtering:

// Defining filter functions
public void customFilterList(String filter){
        tempListArray.clear();
        for (String item : listArray){
            if (item.toLowerCase().contains(filter.toLowerCase())){
                tempListArray.add(item);
            }
        }
        currentFilter = filter;
        tempAdapter.notifyDataSetChanged();
    }
    public void updateList(){
        customFilterList(currentFilter);
    }

I used it for implementing a search function into my list.

The big advantage: You have much more filtering power: at customFilterList() you can easily implement the usage of Regular Expressions or use it as a case sensitive filter.

Instead of notifyDataSetChanged you would call updateList() .

Instead of myListView.getFilter().filter('filter text') you would call customFilterList('filter text')

At the moment this is only working with one ListView. Maybe - with a bit of work - you could implement this as a Custom Array Adapter.

I was struggling with the same issue. Then I found this post on StackOverflow.

In the answer @MattDavis posted, the incoming arraylist is assigned to 2 different arraylists.

 public PromotionListAdapter(Activity a, ArrayList<HashMap<String, String>> d) 
{
    activity = a;
    inflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    imageLoader = new ImageLoader(activity.getApplicationContext());

    //To start, set both data sources to the incoming data
    originalData = d;
    filteredData = d;
}

originalData and filteredData.

originalData is where the original arraylist is stored that was passed in, while filtered data is the dataset that is used when getCount and getItem are called.

In my own ArrayAdapter I kept returning the original data when getItem was called. Therefore the ListView basically would call getCount() and see that the amount of items match my original data (not the filtered data) and getItem would get the requested indexed item from the original data, therefore completely ignoring the new filtered dataset.

This is why it seemed like the notifyDatasetChanged call was not updating the ListView, when in fact it just kept updating from the original arraylist, instead of the filtered set.

Hope this helps clarify this problem for someone else in the future.

Please feel free to correct me if I am wrong, since I am still learning every day!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top