Creating an Uber Clone - Part XVII - Transcript.pdf
1. Creating an Uber Clone - Part XVII
We finally have a client server application that works but we don't have any "real" functionality. To get to that point we’ll need some webservices…
4. public static final String GOOGLE_DIRECTIONS_KEY = "----";
public static final String GOOGLE_GEOCODING_KEY = "----";
public static final String GOOGLE_PLACES_KEY = "----";
New Fields in Globals
All of these API's require developer keys which you can obtain from their respective websites. I've edited the Globals class to include these new keys required by the 3
API’s. Make sure to replace the dashes with the relevant keys. You can get the keys by going to the directions, geocoding and places websites and follow the process
there.
6. {
"results" : [
{
"address_components" : [
{
"long_name" : "277",
"short_name" : "277",
"types" : [ "street_number" ]
},
{
"long_name" : "Bedford Avenue",
"short_name" : "Bedford Ave",
"types" : [ "route" ]
},
... trimmed ...
],
"formatted_address" : "277 Bedford Avenue, Brooklyn, NY 11211, USA",
"geometry" : {
"location" : {
"lat" : 40.714232,
"lng" : -73.9612889
Reverse Geocoding - Result
The result of that URL look like this response JSON. Lets go over two important pieces…
We need to get this result array from the response, we only care about the first element and will discard the rest.
7. {
"results" : [
{
"address_components" : [
{
"long_name" : "277",
"short_name" : "277",
"types" : [ "street_number" ]
},
{
"long_name" : "Bedford Avenue",
"short_name" : "Bedford Ave",
"types" : [ "route" ]
},
... trimmed ...
],
"formatted_address" : "277 Bedford Avenue, Brooklyn, NY 11211, USA",
"geometry" : {
"location" : {
"lat" : 40.714232,
"lng" : -73.9612889
Reverse Geocoding - Result
This is the only attribute we need at this time from this API. Now that we know what we are looking for lets look at the code that accomplishes this.
8. public class SearchService {
private static ConnectionRequest lastLocationRequest;
public static void nameMyCurrentLocation(Location l, SuccessCallback<String> name) {
if(l == null) {
return;
}
if(lastLocationRequest != null) {
lastLocationRequest.kill();
}
lastLocationRequest = Rest.get("https://maps.googleapis.com/maps/api/geocode/json").
queryParam("latlng", l.getLatitude() + "," + l.getLongitude()).
queryParam("key", GOOGLE_GEOCODING_KEY).
queryParam("language", "en").
queryParam("result_type", "street_address|point_of_interest").
getAsJsonMap(callbackMap -> {
Map data = callbackMap.getResponseData();
if(data != null) {
List results = (List)data.get("results");
if(results != null && results.size() > 0) {
Map firstResult = (Map)results.get(0);
name.onSucess((String)firstResult.get("formatted_address"));
}
}
});
}
SearchService
I'll use the SearchService class to encapsulate this functionality for each of these services
9. public class SearchService {
private static ConnectionRequest lastLocationRequest;
public static void nameMyCurrentLocation(Location l, SuccessCallback<String> name) {
if(l == null) {
return;
}
if(lastLocationRequest != null) {
lastLocationRequest.kill();
}
lastLocationRequest = Rest.get("https://maps.googleapis.com/maps/api/geocode/json").
queryParam("latlng", l.getLatitude() + "," + l.getLongitude()).
queryParam("key", GOOGLE_GEOCODING_KEY).
queryParam("language", "en").
queryParam("result_type", "street_address|point_of_interest").
getAsJsonMap(callbackMap -> {
Map data = callbackMap.getResponseData();
if(data != null) {
List results = (List)data.get("results");
if(results != null && results.size() > 0) {
Map firstResult = (Map)results.get(0);
name.onSucess((String)firstResult.get("formatted_address"));
}
}
});
}
SearchService
There is an edge case where location isn't ready yet when this method is invoked, in this case I found it best to just do nothing. Usually it's best to fail by throwing an
exception but that is a valid situation to which I have a decent fallback option so I prefer doing nothing
10. public class SearchService {
private static ConnectionRequest lastLocationRequest;
public static void nameMyCurrentLocation(Location l, SuccessCallback<String> name) {
if(l == null) {
return;
}
if(lastLocationRequest != null) {
lastLocationRequest.kill();
}
lastLocationRequest = Rest.get("https://maps.googleapis.com/maps/api/geocode/json").
queryParam("latlng", l.getLatitude() + "," + l.getLongitude()).
queryParam("key", GOOGLE_GEOCODING_KEY).
queryParam("language", "en").
queryParam("result_type", "street_address|point_of_interest").
getAsJsonMap(callbackMap -> {
Map data = callbackMap.getResponseData();
if(data != null) {
List results = (List)data.get("results");
if(results != null && results.size() > 0) {
Map firstResult = (Map)results.get(0);
name.onSucess((String)firstResult.get("formatted_address"));
}
}
});
}
SearchService
If we send two such calls in rapid succession I only need the last one so I'm canceling the previous request
11. public class SearchService {
private static ConnectionRequest lastLocationRequest;
public static void nameMyCurrentLocation(Location l, SuccessCallback<String> name) {
if(l == null) {
return;
}
if(lastLocationRequest != null) {
lastLocationRequest.kill();
}
lastLocationRequest = Rest.get("https://maps.googleapis.com/maps/api/geocode/json").
queryParam("latlng", l.getLatitude() + "," + l.getLongitude()).
queryParam("key", GOOGLE_GEOCODING_KEY).
queryParam("language", "en").
queryParam("result_type", "street_address|point_of_interest").
getAsJsonMap(callbackMap -> {
Map data = callbackMap.getResponseData();
if(data != null) {
List results = (List)data.get("results");
if(results != null && results.size() > 0) {
Map firstResult = (Map)results.get(0);
name.onSucess((String)firstResult.get("formatted_address"));
}
}
});
}
SearchService
The reverse geocode API latlng argument determines the location for which we are looking
12. public class SearchService {
private static ConnectionRequest lastLocationRequest;
public static void nameMyCurrentLocation(Location l, SuccessCallback<String> name) {
if(l == null) {
return;
}
if(lastLocationRequest != null) {
lastLocationRequest.kill();
}
lastLocationRequest = Rest.get("https://maps.googleapis.com/maps/api/geocode/json").
queryParam("latlng", l.getLatitude() + "," + l.getLongitude()).
queryParam("key", GOOGLE_GEOCODING_KEY).
queryParam("language", "en").
queryParam("result_type", "street_address|point_of_interest").
getAsJsonMap(callbackMap -> {
Map data = callbackMap.getResponseData();
if(data != null) {
List results = (List)data.get("results");
if(results != null && results.size() > 0) {
Map firstResult = (Map)results.get(0);
name.onSucess((String)firstResult.get("formatted_address"));
}
}
});
}
SearchService
We get the parsed result as a Map containing a hierarchy of objects the callback is invoked asynchronously when the response arrives
13. public class SearchService {
private static ConnectionRequest lastLocationRequest;
public static void nameMyCurrentLocation(Location l, SuccessCallback<String> name) {
if(l == null) {
return;
}
if(lastLocationRequest != null) {
lastLocationRequest.kill();
}
lastLocationRequest = Rest.get("https://maps.googleapis.com/maps/api/geocode/json").
queryParam("latlng", l.getLatitude() + "," + l.getLongitude()).
queryParam("key", GOOGLE_GEOCODING_KEY).
queryParam("language", "en").
queryParam("result_type", "street_address|point_of_interest").
getAsJsonMap(callbackMap -> {
Map data = callbackMap.getResponseData();
if(data != null) {
List results = (List)data.get("results");
if(results != null && results.size() > 0) {
Map firstResult = (Map)results.get(0);
name.onSucess((String)firstResult.get("formatted_address"));
}
}
});
}
SearchService
This gets the results list from the JSON and extracts the first element from there
14. public class SearchService {
private static ConnectionRequest lastLocationRequest;
public static void nameMyCurrentLocation(Location l, SuccessCallback<String> name) {
if(l == null) {
return;
}
if(lastLocationRequest != null) {
lastLocationRequest.kill();
}
lastLocationRequest = Rest.get("https://maps.googleapis.com/maps/api/geocode/json").
queryParam("latlng", l.getLatitude() + "," + l.getLongitude()).
queryParam("key", GOOGLE_GEOCODING_KEY).
queryParam("language", "en").
queryParam("result_type", "street_address|point_of_interest").
getAsJsonMap(callbackMap -> {
Map data = callbackMap.getResponseData();
if(data != null) {
List results = (List)data.get("results");
if(results != null && results.size() > 0) {
Map firstResult = (Map)results.get(0);
name.onSucess((String)firstResult.get("formatted_address"));
}
}
});
}
SearchService
We extract the one attribute we care about the formatted_address entry and invoke the callback method with this result
15. https://maps.googleapis.com/maps/api/place/autocomplete/json?
input=lev&location=32.072449,34.778613&radius=50000&key=API_KEY
Places Autocomplete
The places autocomplete API is a bit more challenging since this API is invoked as a user types. We'll need the ability to cancel a request just as we would with the
geocoding calls. Caching is also crucial in this case, so we must cache as much as possible to avoid overuse of the API and performance issues.
Lets start by reviewing the API URL and responses. The default sample from google wasn't very helpful so I had to read the docs a bit and came up with this URL.
The search is relevant to a specific location and radius. Otherwise it would suggest places from all over the world which probably doesn't make sense for an Uber style
application. Notice the radius is specified in meters.
The input value is the string for which we would like auto-complete suggestions.
16. {
"predictions" : [
{
"description" : "Levinsky, Tel Aviv-Yafo, Israel",
"id" : "13fd8422602e10c4a7be775c88280b383a15f368",
"matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"structured_formatting" : {
"main_text" : "Levinsky",
"main_text_matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"secondary_text" : "Tel Aviv-Yafo, Israel"
},
"place_id" : "Eh9MZXZpbnNreSwgVGVsIEF2aXYtWWFmbywgSXNyYWVs",
"reference" : "-pQdrYHivUaFGV9GwZkfwp4xkjUL2Z2mpGNXJBv",
Places Autocomplete - Result
This request produces this JSON result.
All predications are again within an array but this time we'll need all of them.
17. {
"predictions" : [
{
"description" : "Levinsky, Tel Aviv-Yafo, Israel",
"id" : "13fd8422602e10c4a7be775c88280b383a15f368",
"matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"structured_formatting" : {
"main_text" : "Levinsky",
"main_text_matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"secondary_text" : "Tel Aviv-Yafo, Israel"
},
"place_id" : "Eh9MZXZpbnNreSwgVGVsIEF2aXYtWWFmbywgSXNyYWVs",
"reference" : "-pQdrYHivUaFGV9GwZkfwp4xkjUL2Z2mpGNXJBv",
Places Autocomplete - Result
The UI would require the text broken down so we need the main text
18. {
"predictions" : [
{
"description" : "Levinsky, Tel Aviv-Yafo, Israel",
"id" : "13fd8422602e10c4a7be775c88280b383a15f368",
"matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"structured_formatting" : {
"main_text" : "Levinsky",
"main_text_matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"secondary_text" : "Tel Aviv-Yafo, Israel"
},
"place_id" : "Eh9MZXZpbnNreSwgVGVsIEF2aXYtWWFmbywgSXNyYWVs",
"reference" : "-pQdrYHivUaFGV9GwZkfwp4xkjUL2Z2mpGNXJBv",
Places Autocomplete - Result
And we'll need the secondary text too
19. {
"predictions" : [
{
"description" : "Levinsky, Tel Aviv-Yafo, Israel",
"id" : "13fd8422602e10c4a7be775c88280b383a15f368",
"matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"structured_formatting" : {
"main_text" : "Levinsky",
"main_text_matched_substrings" : [
{
"length" : 3,
"offset" : 0
}
],
"secondary_text" : "Tel Aviv-Yafo, Israel"
},
"place_id" : "Eh9MZXZpbnNreSwgVGVsIEF2aXYtWWFmbywgSXNyYWVs",
"reference" : "-pQdrYHivUaFGV9GwZkfwp4xkjUL2Z2mpGNXJBv",
Places Autocomplete - Result
We'll also need the place_id and the reason for this is a HUGE omission in this API. Notice it has no location information... We will need the place_id value to query again
for the location
20. public static class SuggestionResult {
private final String mainText;
private final String secondaryText;
private final String fullText;
private final String placeId;
public SuggestionResult(String mainText, String secondaryText,
String fullText, String placeId) {
this.mainText = mainText;
this.secondaryText = secondaryText;
this.fullText = fullText;
this.placeId = placeId;
}
public String getPlaceId() {
return placeId;
}
public String getMainText() {
return mainText;
}
public String getSecondaryText() {
return secondaryText;
}
public String getFullText() {
return fullText;
}
SuggestionResult
Before we move on to the code we'll need a way to send the results back. We can do that with a list of SuggestionResult entries.
This is a pretty trivial class and doesn't require any explaining.
21. public String getMainText() {
return mainText;
}
public String getSecondaryText() {
return secondaryText;
}
public String getFullText() {
return fullText;
}
public void getLocation(SuccessCallback<Location> result) {
Rest.get("https://maps.googleapis.com/maps/api/place/details/json").
queryParam("placeid", placeId).
queryParam("key", GOOGLE_PLACES_KEY).
getAsJsonMap(callbackMap -> {
Map r = (Map)callbackMap.getResponseData().get("result");
Map geomMap = (Map)r.get("geometry");
Map locationMap = (Map)geomMap.get("location");
double lat = Util.toDoubleValue(locationMap.get("lat"));
double lon = Util.toDoubleValue(locationMap.get("lng"));
result.onSucess(new Location(lat, lon));
});
}
}
SuggestionResult
This class solves the issue of getting the location for a specific entry with the method getLocation. I won't go too much into the details of that code above since it's very
similar to the code we saw before we just get additional details about a place and parse the results. Notice that this is a part of the SuggestionResult class so we don’t
invoke this unless we actually need the location of a place
22. private static ConnectionRequest lastSuggestionRequest;
private static String lastSuggestionValue;
private static final Map<String, List<SuggestionResult>>
locationCache = new HashMap<>();
SearchService
There is one last thing we need before we go into the suggestion method itself. We need variables to cache the data and current request. Otherwise multiple incoming
requests might collide and block the network.
We need the lastSuggestionRequest so we can cancel it.
The lastSuggestionValue lets us distinguish duplicate values this can sometimes happen as an edit event might repeat a request that was already sent for example if a
user types and deletes a character. This can happen since we will wait 500ms before sending characters.
The locationCache reduces duplicate requests. Notice that this can grow to a level of a huge memory leak but realistically that would require a huge number of searches.
If this still bothers you we have the CacheMap class that serializes extra data to storage