Reputation: 575
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
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
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