Benjamin S
Benjamin S

Reputation: 575

Android SearchView: Filterable Listview Runtime Error

I'm running into an occasional issue when using a filterable listview. Infrequently, I'll get a runtime error when deleting text from my searchview while filtering a listview of recipes. It tends to happen when text is deleted quickly using the soft keyboard.

90% of the time, this implementation works exactly as I'd expect, but when deleting characters rapidly I get the following error.

04-07 11:38:55.221    9591-9591/brd.cms.sup E/dalvikvm﹕ >>>>> Normal User
04-07 11:38:55.221    9591-9591/brd.cms.sup E/dalvikvm﹕ >>>>> brd.cms.sup [ userId:0 | appId:10359 ]
04-07 11:38:59.295    9591-9591/brd.cms.sup E/OpenGLRenderer﹕ GL_INVALID_OPERATION
04-07 11:40:58.018    9591-9591/brd.cms.sup E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: brd.cms.sup , PID: 9591
    java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes. [in ListView(2131493035, class android.widget.ListView) with Adapter(class brd.cms.sup.RecipeAdapter)]
            at android.widget.ListView.layoutChildren(ListView.java:1566)
            at android.widget.AbsListView.onLayout(AbsListView.java:2598)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
            at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
            at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.support.v4.widget.DrawerLayout.onLayout(DrawerLayout.java:890)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
            at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.support.v7.internal.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:502)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
            at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
            at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
            at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
            at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
            at android.view.View.layout(View.java:15860)
            at android.view.ViewGroup.layout(ViewGroup.java:4902)
            at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2379)
            at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2092)
            at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1267)
            at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6640)
            at android.view.Choreographer$CallbackRecord.run(Choreographer.java:813)
            at android.view.Choreographer.doCallbacks(Choreographer.java:613)
            at android.view.Choreographer.doFrame(Choreographer.java:583)
            at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:799)
            at android.os.Handler.handleCallback(Handler.java:733)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:146)
            at android.app.ActivityThread.main(ActivityThread.java:5635)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:515)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)
            at dalvik.system.NativeStart.main(Native Method)

Below is my SearchView.OnQueryTextListener implementation.

@Override
public boolean onQueryTextChange(String newText) {
    if (recipeAdapter != null) {
        try {
            recipeAdapter.getFilter().filter(newText);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    return false;
}

Here is my private RecipeFilter in the RecipeAdapter.

private class RecipeFilter extends Filter{
    List<Recipe> filteredList = new ArrayList<>();


    @Override
    protected FilterResults performFiltering(CharSequence constraint){
            constraint = constraint.toString().toLowerCase();
            FilterResults result = new FilterResults();
            filteredList.clear();
            if (constraint != null && constraint.toString().length() > 0) {
                for (Recipe r : backupList) {
                    if (r.contains(constraint)) {
                        filteredList.add(r);
                    }
                }
            } else {
                filteredList.addAll(backupList);
            }
            result.count = filteredList.size();
            result.values = filteredList;
            return result;
        }


    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        recipeList = (List) results.values;
        if (results != null) {
            notifyDataSetChanged();
        }
        else{
            notifyDataSetInvalidated();
        }
    }
}

Any ideas would be greatly appreciated!

Upvotes: 1

Views: 438

Answers (1)

Ifrit
Ifrit

Reputation: 6821

Knowing how you use filteredList and recipeList would help. However your ListView getting out of sync is probably revolved around the lack of synchronizations. The performFiltering() method executes on the background thread. It's imperative that you synchronize any mutations on those Lists...and not just within the performFiltering() method but throughout the entire adapter. Which btw, I sure hope your custom adapter is not an extension of the ArrayAdapter. Which would be another problem in itself.

Here's a short blog post talking about the problems with filtering with Android's ArrayAdapter. At the end of it, you'll find example code showing a good way to write a Filter class for an adapter. Basically the equivalent code you posted here.

Managing Lists

To recap, you have a recipeList which holds either all the items in the adapter or just the filtered items. You also have a backupList which stores all the items. Then within the Filter class, you have a filterList which is used to facilitate the filtered data into recipeList.

This approach is fairly standard...whereby two main lists are used to track both filtered and original data. The key in working with these lists is synchronization and knowing which one to use.

For any getter, your adapter should always work with data from recipeList. Eg, getItem(), getItemId(), etc. Accessing this data does not need to be synchronized.

For any setter, you'll do the following

  1. Synchronize
  2. Determine which list gets updated
  3. Call notifyDataSetChanged

In regards to #2, based on your setup, no matter what your backupList should be updated. Whether or not your recipeList also gets updated depends on whether you want mutations to apply during a filtering or not.

Finally, your filter class needs to add synchronizations. Specifically when you are copying data over from the backupList to filteredList. Additionally, filteredList needs to be local to the method performFiltering() and not global to the class. Either that or synchronize the entire method. Since that method occurs on a background thread, it's very possible for multiple threads to execute that method in tandem. Which would mean they are all changing the same filteredList...which is not good.

Also in publishResults you'll need to synchronize updating recipeList and you need to invoke notifyDataSetInvalidated() when results is either null or empty.

Upvotes: 1

Related Questions