Recently in one of my android applications I wanted to obtain the user’s location in terms of city and country that they can feed from their (edit) profile section. So one way to do this is basically have two dropdowns or dialogs (or even open an entirely new activity where the user can search through entities and select one). One of them would display all the countries in which, once a selection is made, the other one will show a restricted set of cities based on the prior country selection. Now the number of cities in the world is large, so to do this we’ll need to get a database that contains all the countries and cities and then make sure we can query that over HTTP to get the cities based on what the user types into the app (autocomplete box). We’ve to make sure the response is really quick and doesn’t cause lags. We can also bundle all the city and country data into our app but then that’ll blow up the apk size.
Anyway, while searching the web and trying out apps like Airbnb and Couchsurfing I figured Google has the Place Autocomplete service using which I can allow the user to type in a city name which’ll fetch all the cities containing that string from Google’s servers that can be presented in an AutoCompleteTextView.
What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.
So let’s see how to execute this.
Getting an API Key
The first thing to do is to get an API key from Google Developers Console. More on this here. You’ll find four different key options – server, browser, android and iOS – you should go for the browser one as according to their documentation Places API doesn’t work with Android or iOS API key and since the app is like a browser client, a server key doesn’t make sense.
Know your Limits
You should know about the usage limitation. All the information pertaining to that is available here.
Place Autocomplete Requests and Responses
You should go through the Place Autocomplete service documentation where you can learn everything about the requests and responses. Basically you’ll have to make a request to https://maps.googleapis.com/maps/api/place/autocomplete/output?parameters
where output
can be either json
or xml
indicating the type of expected response. There are 2 required GET
parameters which are input
that should contain the string (that the user will type) to use as the search term and key
which should contain your API key. So then the URL will look something like this:
https://maps.googleapis.com/maps/api/place/autocomplete/json?input=ban&key=API_KEY
There are other optional parameters that you can read up on but the most important one that you’ll mostly use is the types
parameter. You can pass various values to types
like:
-
geocode
– Sort of like broad-level (generic) addresses. -
address
– Fully specified addresses. -
establishment
– Business results. -
(regions)
– This will return results matchinglocality
,sublocality
,postal_code
,country
,administrative_area_level_1
,administrative_area_level_2
. Specific details for each of them can be found here. -
(cities)
– Return results matchinglocality
,administrative_area_level_3
.
You can try a JavaScript based Place Autocomplete demo here. In our case we only want cities, so here’s a sample request:
https://maps.googleapis.com/maps/api/place/autocomplete/json?input=ban&types=(cities)&key=API_KEY
Here’s the type of response that you should expect:
{ "predictions" : [ { "description" : "Bangalore, Karnataka, India", "id" : "0862832923832bfb1e46cbe843cdaa03a9ee8aa1", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJbU60yXAWrjsR4E9-UejD3_g", "reference" : "CkQzAAAAUY01eTC-f7Z9vOzDiHFtEL0rLEXfgae0MfOPR8bE26gDDFceZ3AH0SNE44HK7v27noYrRKobbNiWQQ2E4HjRPBIQAM_qIcQgybTqiyKiGyZtkhoUiFs2XXjLEJ3jWUU-p_PGcbm8JH4", "terms" : [ { "offset" : 0, "value" : "Bangalore" }, { "offset" : 11, "value" : "Karnataka" }, { "offset" : 22, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bangarapet, Karnataka, India", "id" : "7496e1dc83b719d83d1f64dcacc1b0592bc1149b", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJu4YuxDfprTsRHSqs7ubXIaw", "reference" : "CkQ0AAAAGsgimTlmv97VHo27L3dRXg0ieWui67PGx84buBY3byBdBecoLmV-IwnW93-RxFRWgQkYRsM3NCQ38UEoKG_VQBIQe7TArn4qHgds5g_Y1Jhl0hoUe0sawRU7Hazq-4jJ8_0RT5coqBY", "terms" : [ { "offset" : 0, "value" : "Bangarapet" }, { "offset" : 12, "value" : "Karnataka" }, { "offset" : 23, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bande Nalla Sandra, Karnataka, India", "id" : "bbdabbca92990bd63dcc6a300d54fdfdcc3f5199", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJseY83P9rrjsRj2k7EToO7Qs", "reference" : "CkQ8AAAA88Q9tpa3SB4oz5t00LapY--mX1kMNOhzO6yQxATNcRTsJvseqWNvu_6xwQm15r55XjV13uvElBa-FHLisNJv-BIQ7jt7wr2lw1Cf4kYuPCXH5BoU3d_k-Oyyh75-uYd-cqsEHZjuCKA", "terms" : [ { "offset" : 0, "value" : "Bande Nalla Sandra" }, { "offset" : 20, "value" : "Karnataka" }, { "offset" : 31, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bandipur, Karnataka, India", "id" : "ba9f9c4fb47c1e65e8b8584a80b05c024c9abab5", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJDQkpSv2tqDsRIBeczAHjVq0", "reference" : "CkQyAAAAEwbLyLxCGnxREVqs067O7MIHJuvr_gUTkBxWoGmZ4XX1xOi8x3krYrL5YelYwn-KdFCQwZw2Usrdr_14loGIfRIQc4CkJu7jq8t0K9VHO7viGxoUacOrVQYFfAS5x8s5bkQbZAQJVXQ", "terms" : [ { "offset" : 0, "value" : "Bandipur" }, { "offset" : 10, "value" : "Karnataka" }, { "offset" : 21, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bangkok Thailand", "id" : "56ef65d942d42054613887fd09cee596d5949359", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJ82ENKDJgHTERIEjiXbIAAQE", "reference" : "CjQoAAAALKjoDM_fiySdIqLK1_B-lWKsH0CyTmaXChd-XYUs6hejfoflIwFfFvUcj12e9Ae3EhC4qhXFaJMMqAIy7-_TJvKKGhQQ7VifQF1Cpn8-bMXCceX0MaQa9A", "terms" : [ { "offset" : 0, "value" : "Bangkok" }, { "offset" : 8, "value" : "Thailand" } ], "types" : [ "locality", "political", "geocode" ] } ], "status" : "OK" }
You’ll note that the description
key has the place name which is sort of in the format “city, state, country” or “city, country”. The place_id
data can be used to make Place Details Requests in which you can get the lat/long any several other details regarding the place.
Adding Place Autocomplete to the Android App
Adding Place Autocomplete to an Android app is fairly easy. All we have to do is issue search requests to the Place API over HTTP and parse the JSON/XML response to display the results in an AutoCompleteTextView
. The requests issued are just like any other request from any type of client like a web browser or a server-side programming language.
Creating the Request Class
We’ll create a class called PlaceAPI
that will send requests to the API over HTTP and then parse the response JSON to build a list of strings containing the description
key which contains the city/state/country names (like “Bangalore, Karnataka, India”). Here’s the class code:
public class PlaceAPI { private static final String TAG = PlaceAPI.class.getSimpleName(); private static final String PLACES_API_BASE = "https://maps.googleapis.com/maps/api/place"; private static final String TYPE_AUTOCOMPLETE = "/autocomplete"; private static final String OUT_JSON = "/json"; private static final String API_KEY = "YOUR_API_KEY"; public ArrayList<String> autocomplete (String input) { ArrayList<String> resultList = null; HttpURLConnection conn = null; StringBuilder jsonResults = new StringBuilder(); try { StringBuilder sb = new StringBuilder(PLACES_API_BASE + TYPE_AUTOCOMPLETE + OUT_JSON); sb.append("?key=" + API_KEY); sb.append("&types=(cities)"); sb.append("&input=" + URLEncoder.encode(input, "utf8")); URL url = new URL(sb.toString()); conn = (HttpURLConnection) url.openConnection(); InputStreamReader in = new InputStreamReader(conn.getInputStream()); // Load the results into a StringBuilder int read; char[] buff = new char[1024]; while ((read = in.read(buff)) != -1) { jsonResults.append(buff, 0, read); } } catch (MalformedURLException e) { Log.e(TAG, "Error processing Places API URL", e); return resultList; } catch (IOException e) { Log.e(TAG, "Error connecting to Places API", e); return resultList; } finally { if (conn != null) { conn.disconnect(); } } try { // Log.d(TAG, jsonResults.toString()); // Create a JSON object hierarchy from the results JSONObject jsonObj = new JSONObject(jsonResults.toString()); JSONArray predsJsonArray = jsonObj.getJSONArray("predictions"); // Extract the Place descriptions from the results resultList = new ArrayList<String>(predsJsonArray.length()); for (int i = 0; i < predsJsonArray.length(); i++) { resultList.add(predsJsonArray.getJSONObject(i).getString("description")); } } catch (JSONException e) { Log.e(TAG, "Cannot process JSON results", e); } return resultList; } }
Next, make sure your layout file (for instance res/layout/activity_main.xml
) has an AutoCompleteTextView:
<!-- City and Country Selector --> <AutoCompleteTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/autocomplete" android:hint="Type in your Location" />
Also create another layout file (res/layout/autocomplete_list_item.xml
) that’ll hold the views representing each entry inside the AutoCompleteTextView:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:id="@+id/autocompleteText" />
Pretty cool! Now your Activity/Fragment class must set the adapter for the AutoCompleteTextView:
AutoCompleteTextView autocompleteView = (AutoCompleteTextView) rootView.findViewById(R.id.autocomplete); autocompleteView.setAdapter(new PlacesAutoCompleteAdapter(getActivity(), R.layout.autocomplete_list_item));
It’s time to code the PlacesAutoCompleteAdapter
class now which should be subtype of ListAdapter
(extends) and Filterable
(implements), if you check the type of parameter specified for the setAdapter()
method in the docs.
class PlacesAutoCompleteAdapter extends ArrayAdapter<String> implements Filterable { ArrayList<String> resultList; Context mContext; int mResource; PlaceAPI mPlaceAPI = new PlaceAPI(); public PlacesAutoCompleteAdapter(Context context, int resource) { super(context, resource); mContext = context; mResource = resource; } @Override public int getCount() { // Last item will be the footer return resultList.size(); } @Override public String getItem(int position) { return resultList.get(position); } @Override public Filter getFilter() { Filter filter = new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); if (constraint != null) { resultList = mPlaceAPI.autocomplete(constraint.toString()); filterResults.values = resultList; filterResults.count = resultList.size(); } return filterResults; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { if (results != null && results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } }; return filter; } }
As the user types something inside the AutoCompleteTextView, the filter()
method on the Filter
object returned by getFilter()
will be called that’ll trigger performFiltering()
(asynchronously in a background thread). This method calls the autocomplete()
method on the `PlaceAPI` class which makes the HTTP calls to the Places API returning results for the string/text entered by the user. The API does a substring match against its database of places and returns a result containing predictions which are basically a list of the places found. The predictions are stored in an ArrayList
which are then used to populate the AutoCompleteTextView internally by the getView()
method of ArrayAdapter
.
You should go through the AutoCompleteTextView source code if you feel like. Also I’ve written an article on the Filter class and Filterable interface before that you should consider reading to get a deep understanding of the filtering works in the piece of code shown above.
Getting the Selection Made
When an item from the AutoCompleteTextView’s list is selected, we can get the data associated with that list item, i.e., the selection made (from the ArrayList used as the data source by the adapter).
autocompleteView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // Get data associated with the specified position // in the list (AdapterView) String description = (String) parent.getItemAtPosition(position); Toast.makeText(getActivity(), description, Toast.LENGTH_SHORT).show(); } });
Show Powered by Google Logo
According to the Places API Policies, if you’re using the Place Autocomplete service without Google Maps, then a powered by Google logo has to be shown. Generally this logo is shown right at the end of the AutoCompleteTextView list items. The logos for various platforms are available in the policies page that you can download and use.
So how to show up the logo in the autocomplete list ? AutoCompleteTextView doesn’t have addHeaderView()
and addFooterView()
methods like the ListView
ViewGroup. So for now, one way to achieve this is by adding an extra element to the ArrayList used as the data source by the adapter and then overriding the getView()
method in the Adapter. In the getView()
if the position
is the last index (equals list.size() - 1
) then inflate a layout which contains an ImageView
whose src
is set to the google logo’s drawable file.
In practise, this is how a quick modification to our adapter implementation would look like:
@Override public View getView(int position, View convertView, ViewGroup parent) { View view; //if (convertView == null) { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (position != (resultList.size() - 1)) view = inflater.inflate(R.layout.autocomplete_list_item, null); else view = inflater.inflate(R.layout.autocomplete_google_logo, null); //} //else { // view = convertView; //} if (position != (resultList.size() - 1)) { TextView autocompleteTextView = (TextView) view.findViewById(R.id.autocompleteText); autocompleteTextView.setText(resultList.get(position)); } else { ImageView imageView = (ImageView) view.findViewById(R.id.imageView); // not sure what to do :D } return view; }
This looks really dirty and doesn’t make use of cache. You should implement the ViewHolder pattern for a better efficient implementation. Also a small line of code will have to be added in the performFiltering()
method:
@Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); if (constraint != null) { resultList = mPlaceAPI.autocomplete(constraint.toString()); // Footer resultList.add("footer"); filterResults.values = resultList; filterResults.count = resultList.size(); } return filterResults; }
Notice the // Footer
comment and the line next to it. This should be enough to abide by their logo requirement policies.
Setting Timeouts for Requests
In the current situation, when the user types something in the box, our code will make API requests to Places Autocomplete service with every character typed (or removed). This can quickly eat up our usage limit. So to reduce the total number of requests made, we’ve to set a timeout for each request as something is typed into the input box. What this means is that when the user is done typing wait for a short time like 500ms or 1000ms (1s) and then make API requests.
So initially I was a little stuck on how to do this. I thought the filtering logic will have to be modified. but then that is sort of not possible. Internally, AutoCompleteTextView attaches a TextWatcher to itself whose afterTextChanged()
method keeps on calling the filter()
method on the Filter object we supply. Now the autocomplete()
method on PlaceAPI
must not be called on the UI/Main thread (since it’s a Network operation) and performFiltering()
already gets called on a background thread which must also return a FilterResults
instantly. So I guess one way to modify the performFiltering()
logic and making it work might be to create a new Handler object inside that (which’ll attach to the background HandlerThread created internally) and then post Runnables to it with a delay. Then return null or an empty FilterResults
object which will then call publishResults
where you pretty much do nothing. When the internal created handler is done executing, post it to a Handler on the main thread. But I think after waiting for 3 seconds or so, the HandlerThread will quit, so this can be a little messy.
Instead what I did was empty the filtering logic and add my own TextWatcher to achieve all of this. The TextWatcher would post the autocomplete operation to a HandlerThread that would do the job in a worker thread asynchronously. First we’ll initialize the HandlerThread and its associated Handler in the constructor.
private static String TAG = MainActivity.class.getSimpleName(); private PlacesAutoCompleteAdapter mAdapter; HandlerThread mHandlerThread; Handler mThreadHandler; public ProfileFragment() { // Required empty public constructor if (mThreadHandler == null) { // Initialize and start the HandlerThread // which is basically a Thread with a Looper // attached (hence a MessageQueue) mHandlerThread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); // Initialize the Handler mThreadHandler = new Handler(mHandlerThread.getLooper()) { @Override public void handleMessage(Message msg) { if (msg.what == 1) { ArrayList<String> results = mAdapter.resultList; if (results != null && results.size() > 0) { mAdapter.notifyDataSetChanged(); } else { mAdapter.notifyDataSetInvalidated(); } } } }; } }
Then add a custom text watcher:
autocompleteView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { final String value = s.toString(); // Remove all callbacks and messages mThreadHandler.removeCallbacksAndMessages(null); // Now add a new one mThreadHandler.postDelayed(new Runnable() { @Override public void run() { // Background thread mAdapter.resultList = mAdapter.mPlaceAPI.autocomplete(value); // Footer if (mAdapter.resultList.size() > 0) mAdapter.resultList.add("footer"); // Post to Main Thread mThreadHandler.sendEmptyMessage(1); } }, 500); } @Override public void afterTextChanged(Editable s) { doAfterTextChanged(); } });
Then finally in onDestroy()
we should quit the HandlerThread:
@Override public void onDestroy() { super.onDestroy(); // Get rid of our Place API Handlers if (mThreadHandler != null) { mThreadHandler.removeCallbacksAndMessages(null); mHandlerThread.quit(); } }
To learn more about HandlerThreads, you should read this article that I wrote sometime back.
Make sure the filtering logic, i.e., performFiltering()
and publishResults()
are empty. You can return null or an empty FilterResults
object from performFiltering()
.
Now both the internal TextWatcher and the one you add will keep on executing. The internal one will try to filter but since the filtering logic is empty, nothing will happen. The custom one that we added will do the actual filtering by modifying the data source (ArrayList) of the Adapter and triggering the notifyDataSetChanged()
.
API Keys
There’s some information regarding where you should store your API Key at the end of this article that you should go through once.
Summary
The Google Place Autocomplete service makes it really simple to integrate a widget where the user can select his location that the app can store and later use for various purposes. If you think about it carefully, Google’s service is only being used to fetch a set of results in JSON format. So replacing that with your own API service will be fairly easy in terms of Android code, i.e., you’ll just need to make few changes in the PlaceAPI
class. This is the power of separation and abstraction. If you think you’ll surpass Google’s request limit or cannot afford them, then consider building your own service using some free/paid databases provided by various vendors online.
References:
- https://developers.google.com/places/training/autocomplete-android
- https://developers.google.com/places/documentation/autocomplete
Hey thanks for this tutorials, I’ve used your code to add a “powered by Google” footer in my autocomplete.
I used the ViewHolder pattern, here is the code if somebody wants it: http://pastebin.com/AB8d2Jjk
Very nice article. Thank you very much for writing it!
I have a question though.
How can you call mAdapter.notifyDataSetChanged(); from a background thread?
That will throw an exception.
I’m not sure whether the question is in the article’s context or general but I’ll answer both.
Firstly, you should always call that method from the main thread, can’t call from UI thread.
Secondly, in the context of this article,
publishResults()
is executed on the main thread. I’ve written an article on filters that can be found here. You’ll also find thenotifyDataSetChanged()
in theHandler
but in that case the Handler is defined in the main thread, hence thehandleMessage()
method will be called on the main thread. More information in this article.I was talking about the HandlerThread (mHandlerThread) which you use to get a Handler (mThreadHandler) by writing “new Handler(mHandlerThread.getLooper())”.
As far as I know, instantiating a HandlerThread, starts a new (different from UI) thread. Getting a handler from HandlerThread mHandleThread’s looper ties it with that thread.
Am I mistaken?
Hello again,
Do you mind answering to the reply I gave you please? I’m trying to implement something based on this but I’m having a tough time understanding how the piece of code I was talking about works.
Why don’t you read the articles I pointed in my comment ? HandlerThread executes your code in a different thread, that is correct. But the adapter is changed inside the Handler’s (not HandlerThread’s) handlerMessage() method which is bound to the main thread since the Handler was “created” in the main thread.
Actually, I have read almost all of your posts.
But I think we are not understanding each other.
HandlerThread thread = new HandlerThread("newThread");
thread.start();
Handler mHandler = new Handler(thread.getLooper()) {
@Override
public void handleMessage(Message msg) {
// Check too seee which thread this code is running on
if (Looper.myLooper() == Looper.getMainLooper()){
System.out.println("THIS IS RUNNING ON MAIN THREAD");
}else{
System.out.println("THIS IS NOT RUNNING ON MAIN THREAD");
}
}
};
Basically, what you are telling me is that the piece of code above (that is identical to the code you wrote in this article) executes in the main thread ( the thread in which mHandler was created) when in fact, it does not.
Everywhere I tested the piece of code above, console prints “THIS IS NOT RUNNING ON MAIN THREAD”. Can you please clue me in as to why ?
Ok, I know what you mean now, given your if/else condition. So according to your if/else condition (I’ve also tested), the code is actually not running on the main thread. To me this is weird due to a couple of observations. You can notify data set change on the adapter or even show a toast message in handleMessage(). These are operations that you’re only allowed to do on a main thread if I’m not mistaken.
At the same time your if/else shows that the thread is not the UI thread. Also if I run an infinite loop, the app won’t crash (which should happen if the thread is the main one).
I’m not sure what am I missing, at least my understand is/was, that the handleMessage() will be called on the thread in which the Handler is defined. Now when I go through the source code of Android’s Handler class I cannot verify my assumptions since I’m unable to find the exact place from which the message is supposed to be dispatched to handleMessage(). I might spend some time inspecting this later.
It is quite possible that handleMessage() should be called on the same thread as the one passed to it in the constructor (HandlerThread’s thread in this case), i.e., the same as the one used to do the execution of the Runnable, but then why should notifications on the adapter or Toast message work ?
Why don’t you take it up on StackOverflow or some other Android coding related forum ? It could presumably be a bug in Android itself or our understanding is wrong ? I’d also eagerly like to know the answer to this problem.
I don’t know have much experience with Threads, Loopers and Message Queues, but from what I’ve been able to find out almost with certainty is that if you declare
Handler mHandler = new Handler(mBackgroundThread.getLooper()); that handler will be associated with the mBackgroundThread. You can actually set a TAG for mBackgroundThread and then check the TAG of the currently running thread inside onHandleMessage and you will see it’s the same.
Now about the notifyDataSetChanged(). What I found out is that (from testing your code above) when you call:
mAdapter.resultList = mAdapter.mPlaceAPI.autocomplete(value);
Because resultList is the dataset for your adapter, everytime it is changed, the adapter automatically calls its notifyDataSetChanged() method from whatever thread the adapter is on. That is a fact that you can verify. Everytime you change the dataset of the adapter, the adapter automatically calls this method to update itself without the need for it to call yourself.
I know this doesn’t explain the ability to call it yourself or to show a Toast message (I have not tested these things) but I wanted to let you know of this. I will post a question to stackoverlow soon and let you know if I find anything. I’d appreciate it if you could do the same.
Thanks!