Question

I have an AutoCompleteTextView with a custom ArrayAdapter that uses ArrayList<Product>.

I've came to the conclusion that a custom ArrayAdapter of an AutoCompleteTextView must implements Filterable and you have to make your own Filtering.

From this SO-question & accepted answer and this example, I have made the following ArrayAdapter:

public class AutoCompleteAdapter extends ArrayAdapter<Product> implements Filterable
{
    // Logcat tag
    private static final String TAG = "AutoCompleteAdapter";

    // The OrderedProductItem we need to get the Filtered ProductNames
    OrderedProductItem orderedProductItem;

    private Context context;
    private ArrayList<Product> productsShown, productsAll;

    // Default Constructor for an ArrayAdapter
    public AutoCompleteAdapter(Context c, int layoutId, ArrayList<Product> objects, OrderedProductItem opi){
        // Though we don't use the Layout-ResourceID , we still need it for the super
        super(c, layoutId, objects);

        L.Log(TAG, "AutoCompleteAdapter Constructor", LogType.VERBOSE);

        // ArrayAdapter's setNotifyOnChange is true by default,
        // but I set it nonetheless, just in case
        setNotifyOnChange(true);

        context = c;
        replaceList(objects, true);
        orderedProductItem = opi;
    }

    // Setup the ListItem's UI-elements
    @Override
    public View getView(int position, View convertView, ViewGroup parent){
        return createTextViewAsItem(position);
    }
    @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent){
        return createTextViewAsItem(position);
    }
    // To prevent repetition, we have this private method
    private TextView createTextViewAsItem(int position){
        TextView label = new TextView(context);
        String name = "";
        if(productsShown != null && productsShown.size() > 0 && position >= 0 && position < productsShown.size() - 1)
            name = productsShown.get(position).getName();
        label.setText(name);

        return label;
    }

    // Replace the List
    // When the boolean is set, we replace this ArrayAdapter's List entirely,
    // instead of just the filtering
    @SuppressWarnings("unchecked")
    public void replaceList(ArrayList<Product> p, boolean replaceInitialList){
        if(p != null && p.size() > 0){
            productsShown = p;
            if(replaceInitialList)
                productsAll = (ArrayList<Product>)productsShown.clone();
            notifyDataSetChanged();
        }
    }

    // Since we are using an AutoCompleteTextView, the Filtering has been reset and we need to apply this ourselves..
    Filter filter = new Filter(){
        @Override
        public String convertResultToString(Object resultValue){
            return ((Product)resultValue).getName();
        }

        @Override
        protected FilterResults performFiltering(CharSequence constraint){
            FilterResults filterResults = new FilterResults();
            if(productsAll != null){
                // If no constraint is given, return the whole list
                if(constraint == null){
                    filterResults.values = productsAll;
                    filterResults.count = productsAll.size();
                }
                else if(V.notNull(constraint.toString(), true)){
                    L.Log(TAG, "performFiltering: " + constraint.toString(), LogType.VERBOSE);

                    ArrayList<Product> suggestions = new ArrayList<Product>();

                    if(p.size() > 0)
                        for(Product p : productsAll)
                            if(p.getName().toLowerCase(Locale.ENGLISH).contains(constraint.toString().toLowerCase(Locale.ENGLISH)))
                                suggestions.add(p);

                    filterResults.values = suggestions;
                    filterResults.count = suggestions.size();
                }
            }
            return filterResults;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            if(results != null && results.count > 0)
                replaceList((ArrayList<Product>)results.values, false);
        }
    };

    @Override
    public Filter getFilter(){
        return filter;
    }
}

Everything works perfectly. However, since I have a list of around 1250 Products and these are all looped every time the User changes his input in the AutoCompleteTextView, including the creation of two new instantiations (FilterResults and ArrayList), I was wondering if there is a better solution for this without having to loop though everything on every user input change.

If there isn't I just keep this. I was just wondering since with an AutoCompleteTextView containing around 1250 objects, with a custom ArrayAdapter (including custom Filtering) and a custom TextWatcher, it isn't that good for the performance. Especially since this AutoCompleteTextView is used inside the item of a ListView. Which means I have an AutoCompleteTextView for every item (potentially ranging from ~ 5 to 50, with an average of around 15).

Was it helpful?

Solution

This is coming fairly late, however I thought I'd weigh in on your problem...mostly because the rather scaring things occurring with your implementation. To answer your immediate question, there's not much you can easily do to avoid the full ArrayList iteration when filtering. If you need something faster, you'll need to look into pre-processing your data into something with faster search times. AutoComplete Algorithm?.

I have a general rule of thumb for customizing the ArrayAdapter filtering logic. Don't do it. Whenever you run into this situation, the correct solution is to roll your own adapter solution (Using BaseAdapter)...or find a 3rd party solution that allows you too. Part of the issue is that internally the ArrayAdapter has it's own two lists for filtering and it's own internal synchronized lock. Your AutoCompleteAdapter is exposing a ton of mutators, all of which synchronize on an object you can't sync on. That means you risk concurrency issues if the adapter is mutated while filtering is occurring.

As it stands with your code, the ArrayAdapter is linked up with your productsAll list. Any mutations, accessors, methods etc will always reference that list. At first I was surprised your solution worked! Then I realized you aren't using getItem as is the norm. I imagine you are completely neglecting all the other ArrayAdapter methods, else you'd have seen rather strange behavior. If that's the case, ArrayAdapter isn't really doing anything for you and you're loading up this huge class for nothing. Would be trivial to switch it out with BaseAdapter.

In fact I'm surprised you aren't seeing other strange problems. For instance, no matter what your filtered list shows, your adapter is always registering the productsAll list count instead of the productsShown count. Which may be why you have all these index out of bounds checks? Typically not needed.

I'm also surprised your filtering operation updates the list since you fail to invoke notifyDataSetChanged when finished.

Next big problem, you should never nest adapters. I'm usually advocating this because people embed ListViews...which is another no no of itself. This is the first I've heard about nesting with AutoCompleteTextView though. Slightly different situation, yet I'd still say this is a bad idea. Why? There's no guarantee how many times getView will be invoked for a given position. It could call it once...it could call it 4 times...or more. So imagine recreating your adapter 4 times per item. Even if only 10 items display at a time, you're looking at 40 instantiations of your custom adapter! I sure hope you figured out a way to recycle those adapters to lower that number.

However considering you aren't using the ViewHolder I'm assuming you don't even know about the recycling behavior? ViewHolder is a must do for any adapter. It single handily will provide an enormous performance boast. Right now, you are creating a new view with every getView invocation and ignoring any of the recycled views provided. There a million examples online that show and explain the ViewHolder. Here's one such link.

Side note, ArrayAdapter already implements Filterable. Re-adding the implements in your custom adapter is not needed.

To sum up:

  • Implement BaseAdapter instead
  • Don't embed adapters. Find a different way to display your UI without requiring multiple AutoCompleteTextViews inside of a ListView.
  • Can't really improve your filtering logic without some heavy data pre-processing.
  • Use ViewHolder paradigm.

Must watch video from Google I/O about ListViews and adapters. Here's some further readings about the ArrayAdapter.

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