views:

106

answers:

2

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?

A: 

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.

LordTwaroog
Yes, I still call notifyDataSetChanged hoping that it would solve my problem. But even that seems to be ignored. I know that I am still able to modify the list even after filtering-then-unfiltering to restore the list to its original state. However any changes I make to the underlying data set of the adapter/list won't show up on screen.
jhie
+1  A: 

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!

jhie