0

This is frustrating, I have spent a week trying to find out how to increase the performance of a custom autocompleteTextView in Android and come out with nothing. I have even implemented a Trie to keep some indexes of the list items in order to recover them when filtering, but in vain (I guess I am using it improperly or the problem is somewhere else). I have implemented a FilterSearchTaskFragment in order to keep a reference to the last started task and to stop it when the user types fastly many characters in the autocompleteTextView.

Inside the performFiltering() method and the publishResults() I have put some controls to recover the state of the actual running task (with a call to the isCancelled() method) in order to make it finish as soon as possible but nothing, or better I have increased the performance a little bit when I test the app in the ADT simulator, but on a real device (my tablet) it seems like things remained the same... Why? What am I messing?

I have googled in order to find some useful answers but I didn't find anyone with the same problem. I have even asked around here on SO and someone told me to use a Loader, someone told me to use a Handler, but when the situation came to facts, no one replied to me and I have no idea on how I can use them in my situation. I am using a custom List of objects (I am filtering some municipalities data which I previously recover from a MySQL DB). Here is all the code I wrote:

First the FilterSearchTaskFragment:

public class FilterSearchTaskFragment extends Fragment {

private TaskCallbacks filterSearchTaskCallbacks;

private static int taskNum;

public interface TaskCallbacks {
    void onPreExecute();
    void onPostExecute();
    void performFilterSearch();
}

private AsyncFilterSearchAbstractTask filterSearchTask;
private boolean running;

@Override 
public void onAttach(Activity activity) {
    super.onAttach(activity);
    if (activity instanceof TaskCallbacks) {
        filterSearchTaskCallbacks = (TaskCallbacks) activity;
    }
    else {
        throw new ClassCastException(activity.toString() + " must implement the FilterSearchTaskFragment.TaskCallbacks interface!");
    }
}

@Override 
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
}

// Start the background filter search task
public void start() {
    filterSearchTask = new AsyncFilterSearchAbstractTask();
    filterSearchTask.execute();
    taskNum++;
    running = true;
}

public int getTaskNum() {
    return taskNum;
}

// Cancel the execution of the background filter search task
public void cancel() {
    filterSearchTask.cancel(true);
    filterSearchTask = null;
    running = false;
}

public boolean isRunning() {
    return this.running;
}

public boolean isCancelled() {
    if (filterSearchTask.isCancelled()) {
        return true;
    }
    return false;
}

// Background filter search 
private class AsyncFilterSearchAbstractTask extends AsyncTask<Void, Void, Void> {

    @Override 
    protected void onPreExecute() {
        filterSearchTaskCallbacks.onPreExecute();
        running = true;
    }

    @Override
    protected Void doInBackground(Void... nothings) {
        filterSearchTaskCallbacks.performFilterSearch();
        return null;
    }

    @Override
    protected void onCancelled() {
        Log.i("TASK CANCELLED", "THE TASK NUMBER " + getTaskNum() + " HAS BEEN CANCELLED");
    }

    @Override
    protected void onPostExecute(Void nothing) {
        filterSearchTaskCallbacks.onPostExecute();
        running = false;
    }
}
 }

It has an interface (TaskCallbacks) which my Activity implements:

public void onPreExecute() {    
}

public void performFilterSearch() {
    if (!filterSearchTask.isCancelled()) {
        adapter.setFilterSearchTask(filterSearchTask);
        adapter.setConstraint(constraint);
        adapter.setStartChangeOffset(startChangeOffset);
        adapter.setBeforeChars(beforeChars);
        adapter.setCountCharsThatReplacedBefore(countCharsThatReplacedBefore);
        adapter.setNotifyOnChange(false);
        if (!filterSearchTask.isCancelled()) {
            adapter.getFilter().filter(constraint);
        }
    }
}

public void onPostExecute() {

}

The filter work is delegated to a Custom Adapter, which contains the Filter implementation, here is the class:

public class MunicipalitySearchAdapter extends ArrayAdapter<Municipality> {

private ArrayList<Municipality> municipalities;
private ArrayList<Municipality> allMunicipalities;
private ArrayList<Municipality> suggestedMunicipalities;

FilterSearchTaskFragment filterSearchTask;

private List<Trieable> triableList;

private Trie municipalityTrie;

private int viewResourceId;

@SuppressWarnings("unchecked")
public MunicipalitySearchAdapter(Context context, int viewResourceId, ArrayList<Municipality> municipalities) {
    super(context, viewResourceId, municipalities);
    this.municipalities = municipalities;
    this.allMunicipalities = (ArrayList<Municipality>) this.municipalities.clone();
    this.suggestedMunicipalities = new ArrayList<Municipality>();
    this.viewResourceId = viewResourceId;
    this.triableList = new ArrayList<Trieable>();
    for (Municipality mun : allMunicipalities) {
        triableList.add(mun);
    }
    municipalityTrie = new Trie(triableList, Locale.ITALY);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View v = convertView;
    if (v == null) {
        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        v = inflater.inflate(this.viewResourceId, null);
    }
    Municipality municipality = municipalities.get(position);
    if (municipality != null) {
        TextView munNameTxtView = (TextView) v.findViewById(R.id.name);
        TextView proSignTxtView = (TextView) v.findViewById(R.id.sign);
        TextView regNameTxtView = (TextView) v.findViewById(R.id.regionName);

        if (munNameTxtView != null) {
            munNameTxtView.setText(municipality.getName());
        }
        if (proSignTxtView != null) {
            proSignTxtView.setText(municipality.getProvinceSign());
        }
        if (regNameTxtView != null) {
            regNameTxtView.setText(municipality.getRegionName());
        }
    }
    return v;
}

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

Filter municipalityFilter = new Filter() {      

    @Override
    public String convertResultToString(Object resultValue) {
        String str = ((Municipality) (resultValue)).getName();
        return str;
    }

    @Override
    protected FilterResults performFiltering(CharSequence constraint) {
        FilterResults filterRes = new FilterResults();

        if (constraint == null || constraint.length() == 0) {
            filterRes.values = allMunicipalities;
            filterRes.count = allMunicipalities.size();
        }

        else {
            String constraintString = constraint.toString().trim().toLowerCase(Locale.ITALY);
            suggestedMunicipalities.clear();

            List<Integer> wordsIndexesList = municipalityTrie.getWordsIndexes(municipalityTrie.getRootVertex(), constraintString);                  
            if (wordsIndexesList != null) {
                for (int i = 0; i<wordsIndexesList.size() && !filterSearchTask.isCancelled(); i++) {
                    int index = wordsIndexesList.get(i);
                    suggestedMunicipalities.add(allMunicipalities.get(index));
                }
            }

            List<Integer> prefixesIndexesList = municipalityTrie.getPrefixesIndexes(municipalityTrie.getRootVertex(), constraintString);
            if (prefixesIndexesList != null) {
                for (int i = 0; i<prefixesIndexesList.size() && !filterSearchTask.isCancelled(); i++) {
                    int index = prefixesIndexesList.get(i);
                    suggestedMunicipalities.add(allMunicipalities.get(index));
                }
            }

            filterRes.values = suggestedMunicipalities;
            filterRes.count = suggestedMunicipalities.size();                   
        }           

        return filterRes;
    }

    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        if (results != null && results.count > 0) {

            @SuppressWarnings("unchecked")
            ArrayList<Municipality> filteredMunicipalities = (ArrayList<Municipality>) results.values;
            ArrayList<Municipality> supportMunicipalitiesList = new ArrayList<Municipality>();
            notifyDataSetChanged();
            clear();
            if (!filterSearchTask.isCancelled()) {
                for (int i = 0; i < filteredMunicipalities.size() && !filterSearchTask.isCancelled(); i++) {
                    Municipality mun = filteredMunicipalities.get(i);
                    supportMunicipalitiesList.add(mun);
                }
                Iterator<Municipality> municipalityIterator = supportMunicipalitiesList.iterator();
                while (municipalityIterator.hasNext() && !filterSearchTask.isCancelled()) {
                    Municipality municipality = municipalityIterator.next();
                    add(municipality);
                }
            }

            notifyDataSetInvalidated();

        }
    }                   
};
}

The Trie class used in the filtering operation is this:

public class Trie {

private Vertex rootVertex;

public Trie(List<Trieable> objectList, Locale locale) {
    rootVertex = new Vertex();

    for (int i = 0; i<objectList.size(); i++) {
        String word = objectList.get(i).toString().toLowerCase(locale);
        addWord(rootVertex, word, i);
    }
}

public Vertex getRootVertex() {
    return rootVertex;
}

public void addWord(Vertex vertex, String word, int index) {
    if (word.isEmpty()) { 
        vertex.addIndexToWordsIndexList(index);
        vertex.increaseWordsNumber();
    }
    else {
        vertex.addIndexToPrefixesIndexList(index);
        vertex.increasePrefixesNumber();
        Character fChar = word.charAt(0);
        HashMap<Character, Vertex> vertexSons = vertex.getVertexSons();

        if (!vertexSons.containsKey(fChar)) {
            vertex.addVertexSon(fChar);
        }

        word = (word.length() == 1) ? "" : word.substring(1);
        addWord(vertexSons.get(fChar), word, index);
    }
}

public List<Integer> getWordsIndexes(Vertex vertex, String word) {
    if (word.isEmpty()) {
        return vertex.getWordsIndexList();
    }
    else {
        Character fChar = word.charAt(0);
        if (!(vertex.getVertexSons().containsKey(fChar))) {
            return null;
        }
        else {
            word = (word.length() == 1) ? "" : word.substring(1);
            return getWordsIndexes(vertex.getVertexSons().get(fChar), word);
        }
    }
}

public List<Integer> getPrefixesIndexes(Vertex vertex, String prefix) {
    if (prefix.isEmpty()) {
        return vertex.getPrefixesIndexList();
    }
    else {
        Character fChar = prefix.charAt(0);
        if (!(vertex.getVertexSons().containsKey(fChar))) {
            return null;
        }
        else {
            prefix = (prefix.length() == 1) ? "" : prefix.substring(1);
            return getPrefixesIndexes(vertex.getVertexSons().get(fChar), prefix);
        }
    }
}

}

Every node is represented by a Vertex of the Trie, this is the class:

public class Vertex {

private HashMap<Character, Vertex> vertexSons;
private List<Integer> wordsIndexList;
private List<Integer> prefixesIndexList;
private int wordsNumber;
private int prefixesNumber;

public Vertex() {
    vertexSons = new HashMap<Character, Vertex>();
    wordsIndexList = new ArrayList<Integer>();
    prefixesIndexList = new ArrayList<Integer>();
    wordsNumber = 0;
    prefixesNumber = 0;
}

public boolean hasWords() {
    if (wordsNumber > 0) {
        return true;
    }
    return false;
}

public boolean hasPrefixes() {
    if (prefixesNumber > 0) {
        return true;
    }
    return false;
}

public void addVertexSon(Character character) {
    vertexSons.put(character, new Vertex());
}

public void addIndexToWordsIndexList(int index) {
    wordsIndexList.add(index);
}

public void addIndexToPrefixesIndexList(int index) {
    prefixesIndexList.add(index);
}

public HashMap<Character, Vertex> getVertexSons() {
    return vertexSons;
}

public List<Integer> getWordsIndexList() {
    return wordsIndexList;
}

public List<Integer> getPrefixesIndexList() {
    return prefixesIndexList;
}

public int getWordsNumber() {
    return wordsNumber;
}

public int getPrefixesNumber() {
    return prefixesNumber;
}

public void increaseWordsNumber() {
    wordsNumber++;
}

public void increasePrefixesNumber() {
    prefixesNumber++;
}
 }

And finally here is where I instantiate the adapter (in my Activity) and add a listener to the AutocompleteTextView:

adapter = new MunicipalitySearchAdapter(getApplicationContext(), R.layout.municipality_list_item, ListaComuni);
                municipalityEditTxt.setAdapter(adapter);
                municipalityEditTxt.addTextChangedListener(new TextWatcher() {

                    @Override
                    public void onTextChanged(CharSequence s, int start, int before, int count) {
                        constraint = s;
                        startChangeOffset = start;
                        beforeChars  = before;
                        countCharsThatReplacedBefore = count;

                        if (filterSearchTask.isRunning()) {
                            filterSearchTask.cancel();
                        }
                        filterSearchTask.start();
                    }

                    @Override
                    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    }

                    @Override
                    public void afterTextChanged(Editable s) {          
                    }
                });

The municipalityEditText is the AutocompleteTextView reference. So please if someone of you here on SO have faced the same problem, could you tell me how did you make it work smoothly??? Please answer me cause I got stuck!

Thanks!

tonix
  • 6,671
  • 13
  • 75
  • 136
  • where do you keep your items to search for? – pskink Mar 29 '14 at 16:28
  • The items are the Municipalities and I keep them in an ArrayList (all of them), but when I create the adapter I create a Trie data structure where each Vertex knows how much words it has and more important stores two lists of type -> List which contains respectively all the position integers of the words that are associated to that branch of the Trie tree (the field is -> private List wordsIndexList;) and all the possible prefixes associated with that Vertex (the field is -> private List prefixesIndexList;). Look at the Vertex class and you find them :). – tonix Mar 29 '14 at 16:40
  • Then When I recover the needed data I simply go to the right branch of the Trie tree and recover the needed indexes. Then I use those indexes to get the allMunicipalities.get(i) to recover the 'nth item of the global list and add it to the suggestion. It shouldn't be so slow because of these, conversely I think this should improve the performance cause I don't iterate over and over the all List but simply move to the right branch of the tree (based on the user input chars) and recover all the needed positions. Beside I guess the problem is somewhere else, what do you think? Could you help me? – tonix Mar 29 '14 at 16:45
  • and how many Municipalities do you have? can you store them in a sqlite table? – pskink Mar 29 '14 at 16:47
  • There are almost 200 Municipalities, so I guess it does not worth the effort to store them in a sqlite. Someone on SO told me to use a Handler or a Loader, do you know if this is a good idea and if yes how can I use one of them in my case? – tonix Mar 29 '14 at 16:59
  • see my answer here http://stackoverflow.com/questions/22443791/multiautocompletetextview-with-contacts-phone-numbers/22465425#22465425 how to search for contact names or their phones, the same you can do in your case, see runQuery method – pskink Mar 29 '14 at 17:03
  • Sorry what you posted is new to me. Are you using an SQLite table and querying it? What is different from my method? – tonix Mar 29 '14 at 17:25
  • you can use an sqlite db and query it, but you can also return a MatrixCursor with the search items, i dont know which one is better for your case, you choose, what is different? it is sooo simple – pskink Mar 29 '14 at 17:29
  • So (just to make my ideas clear): I should store the data in a sqlite table and recover using a cursor, but I have to query always when the user types a char? And another question: I recover the data from a MySQL db and they should change over the time, how can I handle this? – tonix Mar 29 '14 at 17:39
  • 1
    you can use sqlite db but you dont have to, you can return a MatrixCursor, add new rows with newRow() method and add columns with add() method – pskink Mar 29 '14 at 17:46
  • and what, is it faster? – pskink Mar 29 '14 at 19:54
  • Sorry, I didn't implement it yet, I'll try as soon as I can, thanks! – tonix Mar 29 '14 at 21:12

0 Answers0