
Android AutoCompleteTextView with tooltips from a web service
For one of my Android apps, Book Tracker, I implemented a custom AutoCompleteTextView with hints for book titles, which are dynamically loaded with Google Books as you type the name of the book.
The task for the component was as follows:
The final result:

The adapter for AutoCompleteTextView is a key component in which tooltips are loaded and stored. BookAutoCompleteAdapter implements the Filterable interface to intercept user input from an AutoCompleteTextView and pass it as a search request to a web service. The only Filterable interface method is getFilter (), which should return an instance of the Filter class that loads and publishes the data. The descendants of the Filter class must implement two methods: performFiltering (CharSequence constraint) and publishResults (CharSequence constraint, Filter.FilterResults results).
The performFiltering method will be called in a separate thread automatically, so there is no need to create and start a new thread manually. This has already been done for the developer in the Filter class. The publishResults method is called in the UI thread to publish the results on the screen.
When the tips are loaded, a drop-down list with the results will be displayed. Each line consists of two elements: the name of the book and the name of the author.
When using the standard AutoCompleteTextView, a request is triggered after each character entered. If the user types without stopping, the hints received for the previous request may not be relevant when entering each subsequent character. This gives rise to unnecessary and resource-consuming calls to the server, there is a chance of exceeding the API limits that the web service may have, and outdated results loaded for the previous state of the query string are returned.
In order to avoid the above problems, you need to add a slight delay between entering a character and sending a request to the server. If during this delay a person enters the next character, the request for the previous line is canceled and carried forward to the delay time. If the user does not change the line during this time, the request is sent to the server.
To implement the above behavior, you need to create a custom implementation of AutoCompleteTextView and override the performFiltering method (CharSequence text, int keyCode). The mAutoCompleteDelay field defines the time in milliseconds after which the request will be sent to the server if the user has not entered new characters.
It is very important to provide feedback when the user types. You need to show animated progress in the book title input field. Progress is needed to inform the person that the tips are loading and will be displayed soon. Thus, the user will be aware and will be able to wait until they appear. Without such feedback, a person may not even suspect that the field may show prompts.
The ProgressBar and DelayAutoCompleteTextView elements must be placed in a FrameLayout and aligned with the ProgressBar on the right side of the parent group. You also need to initially hide the progress by setting the android: visibility = "gone" attribute.
The ProgressBar connects to the DelayAutoCompleteTextView using the setLoadingIndicator (ProgressBar view) method of the latter. The visibility of the progress element is set in View.VISIBLE when the prompts load and in View.GONE when the download is complete.
Now that all the parts are ready, you need to connect them together:
bookTitle.setThreshold (4) defines the minimum number of characters a user must enter in order for prompts to be displayed.
bookTitle.setLoadingIndicator ((ProgressBar) findViewById (R.id.progress_bar)) connects the ProgressBar to the DelayAutoCompleteTextView.
It is important to set OnItemClickListener for DelayAutoCompleteTextView and assign the correct value to the input field. If this is not done, the result of calling the toString () method of the selected object will be inserted into the field instead of the name of the book.
The task for the component was as follows:
- Data loading should be carried out in a separate stream so as not to block the UI stream;
- Downloading prompts should only begin if the user pauses dialing (to prevent sending multiple requests to the server after each character entered);
- Hints should be loaded if the user entered a string of some minimum length (it makes no sense to start loading data for a string of two or three characters);
- When querying the server, an animated progress should be shown on the right side of the field to inform the user about the download.
The final result:

Step 1 - Implementing a Custom Adapter for AutoCompleteTextView
The adapter for AutoCompleteTextView is a key component in which tooltips are loaded and stored. BookAutoCompleteAdapter implements the Filterable interface to intercept user input from an AutoCompleteTextView and pass it as a search request to a web service. The only Filterable interface method is getFilter (), which should return an instance of the Filter class that loads and publishes the data. The descendants of the Filter class must implement two methods: performFiltering (CharSequence constraint) and publishResults (CharSequence constraint, Filter.FilterResults results).
The performFiltering method will be called in a separate thread automatically, so there is no need to create and start a new thread manually. This has already been done for the developer in the Filter class. The publishResults method is called in the UI thread to publish the results on the screen.
BookAutoCompleteAdapter.java
public class BookAutoCompleteAdapter extends BaseAdapter implements Filterable {
private static final int MAX_RESULTS = 10;
private final Context mContext;
private List mResults;
public BookAutoCompleteAdapter(Context context) {
mContext = context;
mResults = new ArrayList();
}
@Override
public int getCount() {
return mResults.size();
}
@Override
public Book getItem(int index) {
return mResults.get(index);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(mContext);
convertView = inflater.inflate(R.layout.simple_dropdown_item_2line, parent, false);
}
Book book = getItem(position);
((TextView) convertView.findViewById(R.id.text1)).setText(book.getTitle());
((TextView) convertView.findViewById(R.id.text2)).setText(book.getAuthor());
return convertView;
}
@Override
public Filter getFilter() {
Filter filter = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
if (constraint != null) {
List books = findBooks(mContext, constraint.toString());
// Assign the data to the FilterResults
filterResults.values = books;
filterResults.count = books.size();
}
return filterResults;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
mResults = (List) results.values;
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}};
return filter;
}
/**
* Returns a search result for the given book title.
*/
private List findBooks(String bookTitle) {
// GoogleBooksService is a wrapper for the Google Books API
GoogleBooksService service = new GoogleBooksService (mContext, MAX_RESULTS);
return service.findBooks(bookTitle);
}
}
Step 2 - creating XML markup for the tooltip
When the tips are loaded, a drop-down list with the results will be displayed. Each line consists of two elements: the name of the book and the name of the author.
simple_dropdown_item_2line.xml
Step 3 - adding a delay before sending a request to the server
When using the standard AutoCompleteTextView, a request is triggered after each character entered. If the user types without stopping, the hints received for the previous request may not be relevant when entering each subsequent character. This gives rise to unnecessary and resource-consuming calls to the server, there is a chance of exceeding the API limits that the web service may have, and outdated results loaded for the previous state of the query string are returned.
In order to avoid the above problems, you need to add a slight delay between entering a character and sending a request to the server. If during this delay a person enters the next character, the request for the previous line is canceled and carried forward to the delay time. If the user does not change the line during this time, the request is sent to the server.
To implement the above behavior, you need to create a custom implementation of AutoCompleteTextView and override the performFiltering method (CharSequence text, int keyCode). The mAutoCompleteDelay field defines the time in milliseconds after which the request will be sent to the server if the user has not entered new characters.
DelayAutoCompleteTextView.java
public class DelayAutoCompleteTextView extends AutoCompleteTextView {
private static final int MESSAGE_TEXT_CHANGED = 100;
private static final int DEFAULT_AUTOCOMPLETE_DELAY = 750;
private int mAutoCompleteDelay = DEFAULT_AUTOCOMPLETE_DELAY;
private ProgressBar mLoadingIndicator;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
DelayAutoCompleteTextView.super.performFiltering((CharSequence) msg.obj, msg.arg1);
}
};
public DelayAutoCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setLoadingIndicator(ProgressBar progressBar) {
mLoadingIndicator = progressBar;
}
public void setAutoCompleteDelay(int autoCompleteDelay) {
mAutoCompleteDelay = autoCompleteDelay;
}
@Override
protected void performFiltering(CharSequence text, int keyCode) {
if (mLoadingIndicator != null) {
mLoadingIndicator.setVisibility(View.VISIBLE);
}
mHandler.removeMessages(MESSAGE_TEXT_CHANGED);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_TEXT_CHANGED, text), mAutoCompleteDelay);
}
@Override
public void onFilterComplete(int count) {
if (mLoadingIndicator != null) {
mLoadingIndicator.setVisibility(View.GONE);
}
super.onFilterComplete(count);
}
}
Step 4 - Add Animated Progress to the Input Field
It is very important to provide feedback when the user types. You need to show animated progress in the book title input field. Progress is needed to inform the person that the tips are loading and will be displayed soon. Thus, the user will be aware and will be able to wait until they appear. Without such feedback, a person may not even suspect that the field may show prompts.
The ProgressBar and DelayAutoCompleteTextView elements must be placed in a FrameLayout and aligned with the ProgressBar on the right side of the parent group. You also need to initially hide the progress by setting the android: visibility = "gone" attribute.
The ProgressBar connects to the DelayAutoCompleteTextView using the setLoadingIndicator (ProgressBar view) method of the latter. The visibility of the progress element is set in View.VISIBLE when the prompts load and in View.GONE when the download is complete.
Step 5 - Connecting the Components
Now that all the parts are ready, you need to connect them together:
DelayAutoCompleteTextView bookTitle = (DelayAutoCompleteTextView) findViewById(R.id.book_title);
bookTitle.setThreshold(4);
bookTitle.setAdapter(new BookAutoCompleteAdapter(context));
bookTitle.setLoadingIndicator((ProgressBar) findViewById(R.id.progress_bar));
bookTitle.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView adapterView, View view, int position, long id) {
Book book = (Book) adapterView.getItemAtPosition(position);
bookTitle.setText(book.getTitle());
}
});
bookTitle.setThreshold (4) defines the minimum number of characters a user must enter in order for prompts to be displayed.
bookTitle.setLoadingIndicator ((ProgressBar) findViewById (R.id.progress_bar)) connects the ProgressBar to the DelayAutoCompleteTextView.
It is important to set OnItemClickListener for DelayAutoCompleteTextView and assign the correct value to the input field. If this is not done, the result of calling the toString () method of the selected object will be inserted into the field instead of the name of the book.