This practical codelab is part of Unit 11: Location maps and places in Mobile Development You will get the most value out of this course if you work through the codelabs in sequence:
For details about the course, including links to all the concept chapters, apps, codelabs and slides, see Blackboard.
This codelab will take you through the process of using the Google
Places API for Android. This Android SDK is not quite as flexible as
the Web API for Google Places, but allows us to do most things that
the Web API can do.
You should be able to:
Create a new Android Project and select Basic Activity as the template, name the project GooglePlaces and set the Minimum SDK to API: 18 Android 4.3 (Jelly Bean)
build.gradle
file, look at the buildscript.repositories section and make sure Google's Maven repo is listed:buildscript {
repositories {
google()
jcenter()
}
dependencies {
// ... other implementation dependencies
implementation 'com.google.android.libraries.places:places:2.4.0'
// ... testing dependencies
}
Before you start using the Places SDK for Android, you need a project with a billing account and the Places API enabled. To learn more, see Get Started with Google Maps Platform.
The API key is a unique identifier that authenticates requests associated with your project for usage and billing purposes. You must have at least one API key associated with your project.
To create an API key:
There are two kinds of certificates:
We will create a debug certificate for now. Follow the steps below to display a certificate's SHA-1 fingerprint using the keytool program with the -v parameter. For more information about Keytool
, see the Oracle documentation.
debug.keystore
, and is created the first time you build your project (click Build > Rebuild Project in Android Studio now to do that). By default, the keystore file is stored in the same directory as your Android Virtual Device (AVD) files:~/.android/
C:\Users\your_user_name\.android\
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
You should see output similar to this:
Alias name: androiddebugkey
Creation date: Jan 01, 2013
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 4aa9b300
Valid from: Mon Jan 01 08:04:04 UTC 2013 until: Mon Jan 01 18:04:04 PST 2033
Certificate fingerprints:
MD5: AE:9F:95:D0:A6:86:89:BC:A8:70:BA:34:FF:6A:AC:F9
SHA1: BB:0D:AC:74:D3:21:E1:43:07:71:9B:62:90:AF:A1:66:6E:44:5D:75
Signature algorithm name: SHA1withRSA
Version: 3
The line that begins SHA1 contains the certificate's SHA-1 fingerprint. The fingerprint is the sequence of 20 two-digit hexadecimal numbers separated by colons. Make a note of this value because you need it in the next step.
We will now restrict your API key. Restricting API Keys adds security to your application by ensuring only authorized requests are made with your API Key. We strongly recommend that you follow the instructions to set restrictions for your API Keys. For more information, see API Key best practices.
AndroidManifest.xml
in the manifest
tag at the start of the file) and SHA-1 certificate fingerprint. For example:uk.aston.googleplaces
andBB:0D:AC:74:D3:21:E1:43:67:71:9B:62:91:AF:A1:66:6E:44:5D:75
Use the SHA-1 fingerprint that you generated in the steps above.<string name="places_key">PASTE YOUR KEY HERE</string>
MainActivity.java
and add the following two lines to the end of your onCreate
method:// Initialize the SDK
Places.initialize(getApplicationContext(), getResources().getString(R.string.places_key));
// Create a new PlacesClient instance
PlacesClient placesClient = Places.createClient(this);
The autocomplete service in the Places SDK for Android returns place predictions in response to user search queries. As the user types, the autocomplete service returns suggestions for places such as businesses, addresses, plus codes, and points of interest.
The autocomplete widget is a search dialog with built-in autocomplete functionality. As a user enters search terms, the widget presents a list of predicted places to choose from. When the user makes a selection, a Place instance is returned, which your app can then use to get details about the selected place.
CardView
containing the AutocompleteSupportWidget
to your layout:<androidx.cardview.widget.CardView
android:id="@+id/cardview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="144dp">
<fragment
android:id="@+id/autocomplete_fragment"
android:name="com.google.android.libraries.places.widget.AutocompleteSupportFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.cardview.widget.CardView>
AutocompleteSupportWidget
fragment has no border or background. To provide a consistent visual appearance, we nest the fragment within another layout element such as a CardView
.onActivityResult
, you must call super.onActivityResult
, otherwise the fragment will not function properly.Textview
with the id textview_first
so that it is constrained to be below the CardView
app:layout_constraintStart_toStartOf="@id/cardview"
The PlaceSelectionListener
handles returning a place in response to the user's selection.
AutocompleteSupportFragment
. To implement that, add the code below to the end of your FirstFragment
's onViewCreated
:// Get a reference to the TextView
TextView text = view.findViewById(R.id.textview_first);
// Initialize the AutocompleteSupportFragment.
AutocompleteSupportFragment autocompleteFragment = (AutocompleteSupportFragment)
getChildFragmentManager().findFragmentById(R.id.autocomplete_fragment);
// Specify the types of place data to return.
autocompleteFragment.setPlaceFields(Arrays.asList(Place.Field.ID, Place.Field.NAME));
// Set up a PlaceSelectionListener to handle the response.
autocompleteFragment.setOnPlaceSelectedListener(new PlaceSelectionListener() {
@Override
public void onPlaceSelected(@NotNull Place place) {
text.setText("Place: " + place.getName() + ", " + place.getId());
}
@Override
public void onError(@NotNull Status status) {
text.setText("An error occurred: " + status);
}
});
TextView
as shown below:The information we can find out about a place is determined by the place data fields that we ask for. In the code above we specified the following:
// Specify the types of place data to return.
autocompleteFragment.setPlaceFields(Arrays.asList(Place.Field.ID, Place.Field.NAME));
which primes the request to return the place ID and the place name. To get more information returned in a response, we need to add more place fields to the AutocompleteFragment
. Here is a list of the basic data we can obtain.
Field | SDK Type |
Address Component |
|
Business Status |
|
Formatted Address |
|
Viewport |
|
Location |
|
Name |
|
Photo |
|
Place ID |
|
Plus Code |
|
Type |
|
UTC Offset |
|
Note that if the data is not available, the field will not be returned at all. If you ask for photo data and a place has no photos then the field will not be included in the result. Some of the data fields return a list of values, such as the TYPES field which returns all of the types associated with the place. When asking for photo data, what gets returned is not the photos themselves but some metadata. If you want the actual photo, you need to request that separately. The metadata includes data on all the available photos so you can request one or you can request them all and display them in some kind of gallery.
In this section we will get the list of place types.
FirstFragment.java
if it is not already open and edit the list of place types to be set for your AutocompleteFragment so that you include Place.Field.TYPES
:// Specify the types of place data to return.
autocompleteFragment.setPlaceFields(Arrays.asList(Place.Field.ID, Place.Field.NAME, Place.Field.TYPES));
@Override
public void onPlaceSelected(@NotNull Place place) {
StringBuffer typelist = new StringBuffer();
for (Place.Type type: place.getTypes()) {
typelist.append(type.name());
typelist.append(", ");
}
text.setText("Place: " + place.getName() + "" +
"\nID:" + place.getId() +
"\nTypes: " + typelist.toString());
}
In this section we will update the app to request photo data and then to request and display the first photo.
ic_baseline_photo_24
.ImageView
below the autocomplet_fragment
. Add constraints so that the textview_first
is below the ImageView
: <androidx.cardview.widget.CardView
android:id="@+id/cardview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="144dp">
<fragment
android:id="@+id/autocomplete_fragment"
android:name="com.google.android.libraries.places.widget.AutocompleteSupportFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/imageview_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:src="@drawable/ic_baseline_photo_24"
app:layout_constraintBottom_toTopOf="@+id/textview_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cardview" />
<TextView
android:id="@+id/textview_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_first_fragment"
app:layout_constraintBottom_toTopOf="@id/button_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageview_first" />
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/cardview" />
FirstFragment.java
and edit the list of place types to be set for your AutocompleteFragment so that you include Place.Field.PHOTO_METADATAS
:// Specify the types of place data to return.
autocompleteFragment.setPlaceFields(Arrays.asList(Place.Field.ID, Place.Field.NAME, Place.Field.TYPES, Place.Field.PHOTO_METADATAS));
@Override
public void onPlaceSelected(@NotNull Place place) {
// det details of the first photo
if (place.getPhotoMetadatas() != null && place.getPhotoMetadatas().size() > 0) {
PhotoMetadata photo = place.getPhotoMetadatas().get(0);
insertPhoto(imageView, photo);
}
StringBuffer typelist = new StringBuffer();
for (Place.Type type: place.getTypes()) {
typelist.append(type.name());
typelist.append(", ");
}
text.setText("Place: " + place.getName() + "" +
"\nID:" + place.getId() +
"\nTypes: " + typelist.toString());
}
private void insertPhoto(ImageView imageView, PhotoMetadata photoMetadata) {
// Get the attribution text.
final String attributions = photoMetadata.getAttributions();
// Create a FetchPhotoRequest.
final FetchPhotoRequest photoRequest = FetchPhotoRequest.builder(photoMetadata)
.setMaxWidth(500) // Optional.
.setMaxHeight(300) // Optional.
.build();
((MainActivity)getActivity()).getClient().fetchPhoto(photoRequest).addOnSuccessListener((fetchPhotoResponse) -> {
Bitmap bitmap = fetchPhotoResponse.getBitmap();
imageView.setImageBitmap(bitmap);
}).addOnFailureListener((exception) -> {
if (exception instanceof ApiException) {
final ApiException apiException = (ApiException) exception;
Log.e(MainActivity.TAG, "Place not found: " + exception.getMessage());
final int statusCode = apiException.getStatusCode();
// TODO: Handle error with given status code.
}
});
}
If you run your app now and click the button that takes you to the second fragment, when you return to the first fragment, you selection of a place will have disappeared. In order for the FirstFragment
to remember a place that you selected from the Place AutoComplete Widget, you need to implement a ViewModel that will keep track of the current place that has been selected. One benefit of using a ViewModel
to handle your data, is that access to the ViewModel can be shared across different fragments. To illustrate that, we will now extend the app so that when you select a place from the autocomplete widget, the current place is stored in a ViewModel
and then when you go to the SecondFragment
, that Fragment
accesses the ViewModel
to display data about the currently selected place. When you return to the FirstFragment
, that fragment accesses the ViewModel
to restore and display its data on the currently selected place.
Our first step in managing the data is to create a new class.
model
.CurrentPlaceViewModel
.extendViewModel
. Add implementation for this class as shown below:public class CurrentPlaceViewModel extends ViewModel {
private final MutableLiveData<Place> selected = new MutableLiveData<Place>();
public void select(Place item) {
selected.setValue(item);
}
public LiveData<Place> getSelected() {
return selected;
}
}
This provides us with a live data Place
object called selected
. The Place
class is imported from com.google.android.libraries.places.api.model.Place
. There are only two methods. The method called select
receive a Place
as a parameter and assigns that value into our selected
variable. We should call this function when the user selects a place from the autocomplete widget. The method called getSelected
will return the currently selected place as a LiveData
object. This means that fragments can observe
that value and be informed when the currently selected place changes.
CurrentPlaceViewModel
, we need to update FirstFragment
to make use of it. Add a variable to CurrentPlaceViewModel
:private CurrentPlaceViewModel model;
onViewCreated
method, add some code to initialise your model
variable:model = new ViewModelProvider(requireActivity()).get(CurrentPlaceViewModel.class);
FirstFragment
to move all the code to display data about a place (currently that is inside your onPlaceSelected
event handler) into a method, so you can call that method whenever you need to display the details of the current place. Here is my method:private void displayPlaceData(Place place) {
if (place.getPhotoMetadatas() != null && place.getPhotoMetadatas().size() > 0) {
PhotoMetadata photo = place.getPhotoMetadatas().get(0);
insertPhoto(imageView, photo);
}
StringBuffer typelist = new StringBuffer();
for (Place.Type type: place.getTypes()) {
typelist.append(type.name());
typelist.append(", ");
}
text.setText("Place: " + place.getName() + "" +
"\nID:" + place.getId() +
"\nTypes: " + typelist.toString());
}
onPlaceSelected
event handler to call the new displayPlaceData
method and to inform the CurrentPlaceViewModel
that a new place has been selected:@Override
public void onPlaceSelected(@NotNull Place place) {
currentPlace = place;
displayPlaceData(currentPlace);
model.select(currentPlace);
}
@Override
public void onResume() {
super.onResume();
currentPlace = model.getSelected().getValue();
if (currentPlace != null) {
displayPlaceData(currentPlace);
}
}
SecondFragment
and then navigate back to FirstFragment
. You should see that FirstFragment
now restores the current selected place to the UI when it is resumed.SecondFragment
to use CurrentPlaceViewModel
Your layout for SecondFragment
(in fragment_second.xml
) contains a TextView that doesn't show any text
onViewCreated
methodTextView text = view.findViewById(R.id.textview_second);
CurrentPlaceViewModel model = new ViewModelProvider(requireActivity()).get(CurrentPlaceViewModel.class);
model.getSelected().observe(getViewLifecycleOwner(), item -> {
// Update the UI.
text.setText(model.getSelected().getValue().getName());
});
This code gets the CurrentPlaceViewModel
and sets up an observe
method to observe the LiveData
. When the observer is informed that the LiveData
object has a new value, it will update the TextView
to display the name of the selected Place
.SecondFragment
. You should see that the name of the place that you selected in FirstFragment
is now displayed in the UI for SecondFragment. The SecondFragment
has access to the whole Place
object representing the current selected place and so any data from that object could be used in SecondFragment
.The related concept documentation is in 8.1: Places API.
Android developer documentation: