8

I have a list of key words (about 1000 words) and i set this to an ArrayAdapter to be handled by AutoCompleteTextView. The basic process works fine. The problem arise when i selected a long word (10 character above), then use the keyboard backspace button to remove the word (press and hold on the button), after removing like 5 characters the app crashes with the following error.

01-16 13:27:23.082: ERROR/AndroidRuntime(2874): FATAL EXCEPTION: main
        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. [in ListView(-1, class android.widget.AutoCompleteTextView$DropDownListView) with Adapter(class com.hdm_i.dm.corp.android.muenchen.adapters.PoiAutoCompleteAdapter)]
        at android.widget.ListView.layoutChildren(ListView.java:1527)
        at android.widget.AbsListView.onLayout(AbsListView.java:1430)
        at android.view.View.layout(View.java:7228)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:338)
        at android.view.View.layout(View.java:7228)
        at android.view.ViewRoot.performTraversals(ViewRoot.java:1145)
        at android.view.ViewRoot.handleMessage(ViewRoot.java:1865)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:130)
        at android.app.ActivityThread.main(ActivityThread.java:3687)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:507)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:842)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
        at dalvik.system.NativeStart.main(Native Method)  

Below is my code, did i do anything wrong ? Thanks in advance for your suggestions :-)

public class PoiAutoCompleteAdapter extends ArrayAdapter<SearchTextAutoSuggest> implements Filterable {

    private List<SearchTextAutoSuggest> searchTextAutoSuggestList = new ArrayList<SearchTextAutoSuggest>();
    private SearchTextAutoSuggest defaultSuggestion = new SearchTextAutoSuggest();

    private Handler uiThreadHandler;

    public PoiAutoCompleteAdapter(Context context, int viewResourceId, Handler uiThreadHandler) {
        super(context, viewResourceId);
        this.uiThreadHandler = uiThreadHandler;
        defaultSuggestion.setName(AppConstants.DEFAULT_SEARCH_STRING_NAME);
    }

    @Override
    public int getCount() {
        return searchTextAutoSuggestList.size();
    }

    @Override
    public SearchTextAutoSuggest getItem(int position) {
        return searchTextAutoSuggestList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public Filter getFilter() {
        Filter filter = new Filter() {

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                FilterResults filterResults = new FilterResults();
                synchronized (filterResults) {
                    if (constraint != null) {
                        // Clear and Retrieve the autocomplete results.
                        searchTextAutoSuggestList.clear();
                        searchTextAutoSuggestList = getFilteredResults(constraint);

                        // Assign the data to the FilterResults
                        filterResults.values = searchTextAutoSuggestList;
                        filterResults.count = searchTextAutoSuggestList.size();
                    }

                    return filterResults;
                }
            }

            @Override
            protected void publishResults(CharSequence constraint, final FilterResults filterResults) {
                uiThreadHandler.post(new Runnable() {
                    public void run() {
                        synchronized (filterResults) {
                            if (filterResults != null && filterResults.count > 0) {
                                notifyDataSetChanged();
                            } else {
                                Logs.e("Tried to invalidate");
                                notifyDataSetInvalidated();
                            }
                        }
                    }
                });
            }
        };
        return filter;
    }

    private List<SearchTextAutoSuggest> getFilteredResults(CharSequence constraint) {
        List<SearchTextAutoSuggest> searchTextAutoSuggestList = AppContext.searchTextAutoSuggestList;
        List<SearchTextAutoSuggest> filteredSearchTextAutoSuggestList = new ArrayList<SearchTextAutoSuggest>();

        // Assign constraint as a default option into the list
        defaultSuggestion.setLabel(getContext().getString(R.string.general_default_search_str) + " \'" + constraint + "\'");
        filteredSearchTextAutoSuggestList.add(defaultSuggestion);

        for (int i = 0; i < searchTextAutoSuggestList.size(); i++) {
            if (searchTextAutoSuggestList.get(i).getLabel().toLowerCase().startsWith(constraint.toString().toLowerCase())) {
                filteredSearchTextAutoSuggestList.add(searchTextAutoSuggestList.get(i));
            }
        }

        return filteredSearchTextAutoSuggestList;
    }

}

Amit Gupta
  • 8,914
  • 1
  • 25
  • 33
Thilek
  • 676
  • 6
  • 18
  • why do you extend ArrayAdapter? – pskink Jan 16 '14 at 12:55
  • @pskink usually when i do adapter for list/spinner i used array adapter to manage my list..based on some example i saw.. but is there a better way to do it ? – Thilek Jan 16 '14 at 12:59
  • see my answer here http://stackoverflow.com/questions/19858843/how-to-dynamically-add-suggestions-to-autocompletetextview-with-preserving-chara – pskink Jan 16 '14 at 13:03
  • @pskink i noticed you used simpleCursorAdapter, but can it be used with list of objects ? because in my case when user click on the keyword, i need the object to be given so that i can get other parameters attached with the keyword as an object. – Thilek Jan 16 '14 at 13:42
  • sure, you can add whatever you want to the returned Cursor, or you can just use required _id column to store the index of the original objects array – pskink Jan 16 '14 at 13:49

3 Answers3

10

because performFiltering executes in worker thread. And you assign your searchTextAutoSuggestList variable in this thread, but you have to change data of the adapter only in UI thread. Also publishResults method executes in UI thread so you don't need any Handlers here.

@Override
protected FilterResults performFiltering(CharSequence constraint) {
    FilterResults filterResults = new FilterResults();
    synchronized (filterResults) {
        if (constraint != null) {
            // Clear and Retrieve the autocomplete results.
            List<SearchTextAutoSuggest> resultList = getFilteredResults(constraint);

            // Assign the data to the FilterResults
            filterResults.values = resultList;
            filterResults.count = resultList.size();
        }
        return filterResults;
    }
}

@Override
protected void publishResults(CharSequence constraint, final FilterResults filterResults) {
    if (filterResults != null && filterResults.count > 0) {
        searchTextAutoSuggestList.clear();
        searchTextAutoSuggestList = filterResults.values;
        notifyDataSetChanged();
    } else {
        Logs.e("Tried to invalidate");
        notifyDataSetInvalidated();
    }

}
Autocrab
  • 3,474
  • 1
  • 15
  • 15
  • 2
    thanks for the reply.. it works.. the funny thing is, my first implementation was base on an example from google developer site.. Wonder any other users have experienced such problem as mine.. https://developers.google.com/places/training/autocomplete-android – Thilek Jan 16 '14 at 15:14
  • 2
    Hi Thilek, I'm was getting the same error. The problem was we are changing the 'searchTextAutoSuggestList' reference. Instead of searchTextAutoSuggestList = filterResults.values, make searchTextAutoSuggestList.addAll((List)filterResults.values), and syncronize the searchTextAutoSuggestList instance instead of filterResults. – Mael Jan 24 '14 at 16:18
  • @Mael it make sense now. Thanks ;) – Thilek Feb 03 '14 at 14:00
  • instead of calling `notifyDataSetInvalidated()` i was calling `notifyDataSetChanged()` in both cases. Calling the prior one fixed my issue. – Ahmet Gokdayi Mar 12 '20 at 14:43
2

I was facing the same problem and after a lot of debugging and research I solved the problem by overriding notifyDataSetChanged() and evaluating the size of suggestionList. Snippet is as follows.

private int size = 0;

     @Override
        public void notifyDataSetChanged() {
             size = suggestionList.size();
            super.notifyDataSetChanged();
        }

…and then return the size in getCount():

@Override
    public int getCount() {

        return size; // Return the size of the suggestions list.
    }

…and the filter code goes here:

 private class CustomFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            suggestions.clear();
            FilterResults filterResults = new FilterResults();
            try {

                    if (originalList != null && constraint != null) { // Check if the Original List and Constraint aren't null.
                        try {
                            for (int i = 0; i < originalList.size(); i++) {
                                // if (originalList.get(i).toLowerCase().contains(constraint)) {
                                if (originalList.get(i).toLowerCase().contains(constraint.toString().toLowerCase())) {
                                    suggestionList.add(originalList.get(i)); // If TRUE add item in Suggestions.
                                }
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } else {
                        notifyDataSetChanged();
                    }

            } catch (Exception e) {
                e.printStackTrace();
            }
           // Create new Filter Results and return this to publishResults;
            filterResults.values = suggestionList;
            filterResults.count = suggestionList.size();

            return filterResults;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
           if (results != null && results.count > 0) {
                notifyDataSetChanged();
            } else {
                notifyDataSetInvalidated();
            }
        }
    }
sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
Zafar Imam
  • 319
  • 3
  • 11
1

this for my xamarin.android fellows

i found a custom adapter/filter from xamarin examples but there is a bug in that example. the code below is the fixed . problematic code is commented

explanation: the example code was updating adapter in the PerformFilter . but the adapter's notifydatachanged called in the PublishResult . in that delay if user manages to delete text faster than the your filtering algroithm, then BOOOOOOOOOOOOOM!!!!

this is loosly related but: also cheesebaron example is doesnt work publish result input value filterresult.values is always null. that make me lose time.

   class SuggestionsFilter: Filter {

    string[] temp_matchitems_foradapter = new string[1];
    eArrayAdapter customAdapter;
    public SuggestionsFilter(eArrayAdapter adapter): base() {
     customAdapter = adapter;
    }
    protected override Filter.FilterResults PerformFiltering(Java.Lang.ICharSequence constraint) {
     FilterResults results = new FilterResults();
     if (constraint != null) {
      var searchFor = constraint.ToString();

      var matches = customAdapter.AllItems.Where(i =>  i.ToString().IndexOf(searchFor) >= 0 );


      #region  !!!!! FOCUS HEREEEEEEEEEE !!!!!      
      // WRONG----  dont update  adapter here. 
      //adapter.matchitems=  matches.ToArray();
      // RİGHT
      temp_matchitems_foradapter = matches.ToArray();
      #endregion 

      //this doesnt populate filtered view
      results.Values = FromArray(matches.Select(r => r.ToJavaObject()).ToArray());;
      results.Count = matches.Count();
     }
     return results;
    }

    protected override void PublishResults(Java.Lang.ICharSequence constraint, Filter.FilterResults results) 
    {
     //  update customAdapter.matchitems here  and notifychanges
     customAdapter.MatchItems = tempmathitems_foradapter;
     customAdapter.NotifyDataSetChanged();
    }
bh_earth0
  • 2,537
  • 22
  • 24