Chapters

Hide chapters

Android Apprentice

Fourth Edition · Android 11 · Kotlin 1.4 · Android Studio 4.1

Section II: Building a List App

Section 2: 7 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

15. Google Places
Written by Namrata Bandekar

Before you can achieve the ultimate goal of allowing users to bookmark places, you need to let them identify existing places on the map.

In this chapter, you’ll learn how to identify when a user taps on a place and use the Google Places API to retrieve detailed information about the place.

Getting started

If you’re following along with your own app, open it and keep using it with this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the PlaceBook app inside the starter folder. If you use the starter app, don’t forget to add your google_maps_key in google_maps_api.xml. Read Chapter 13 for more details about the Google Maps key.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

If you’re following along with your own app, you’ll also need to copy default_photo.png from src/main/res/drawable-xxx, which is included with the starter project, into your project.

Make sure to copy the files from all of the drawable folders (hdpi,mdpi,xhdpi,xxhdpi).

Before using the Google Places API, you need to take care of a bit of housekeeping first by enabling the Places API in the developer console and adding the Places API dependency.

Note: The Google screens in this book might be slightly different than what you see on the Google developer portal since Google changes these often.

Enable the places API

The Maps SDK for Android was enabled on your Google developer account when you created the initial Google Maps key. However, you need to turn on the Google Places API manually.

Log into your Google developer account at https://console.developers.google.com.

Ensure the project containing the Maps API key you created previously is selected. Switch to the Library tab on the left.

Click on Places API.

You’ll see the following screen with an Enable button:

Click on ENABLE and wait while Google enables the API. After the API is enabled, the screen changes to show your app’s metrics for this API.

Click on the hamburger menu and navigate back to the main Dashboard.

Scroll down to see the APIs for your app. Both the Maps SDK for Android and Places API are listed.

One last thing. To use the Places API, you must enable billing on each of your projects that use the SDK. To do this sign up for billing at https://console.cloud.google.com/projectselector2/billing/enable.

Places API overview

The Google Places API provides a wealth of capabilities all related to — wait for it — working with places on a map! A place is anything that can be identified on a map, such as a household, a business, or a public park. Google places gives you access to over 200 million places stored in the main Google Maps database.

Although referred to as a single API, you generally interact with the Places API through several sub-APIs.

Note: Google Places for Android enforces a limit on the number of requests per month. You can see the details of the pricing model at https://cloud.google.com/maps-platform/pricing. To prevent your app from failing when it exceeds these limits, follow the instructions in the usage limits guide at https://developers.google.com/places/android-sdk/usage-and-billing.

Add the Places API dependency

Just like the location API, you’ll have to add the Places API dependency yourself.

Open app/build.gradle and add the places library to the dependencies section as follows:

implementation "com.google.android.libraries.places:places:2.4.0"

This instructs the Gradle build system to include the Places API in your build.

Selecting points of interest

You may have noticed icons with place names scattered throughout the map. These are called points of interest, or POIs, and they will let the user look up details about each place. You’ll begin by making the POIs a little more interesting by allowing the user to interact with them.

The Google Map object has convenient, built-in capabilities to let you know when the user taps on a POI. You need to set up a POI click listener and wait for the user to tap away.

Like all other interactions with the map object, you’ll wait to set up the listener until OnMapReady() is called. Open MapsActivity.kt and add the following to the end of onMapReady():

map.setOnPoiClickListener {
  Toast.makeText(this, it.name, Toast.LENGTH_LONG).show()
}

Here, you call setOnPoiClickListener() on map and provide it a lambda that implements the single onPoiClick() method of the PoiClickListener interface.

The map object will call your lambda anytime it detects that the user has tapped on a POI. The lambda is passed in a single parameter of type PointOfInterest that you access through the implicit it variable.

PointOfInterest contains only three properties:

  1. latLng: The geographic location of the selected POI is represented by a latitude and longitude in decimal degrees.
  2. name: The name of the POI. This normally matches what’s shown on the map.
  3. placeId: A string that uniquely identifies the POI. You can use the placeId to retrieve a Place object from the Places API.

Run the app and tap on a few places. You’ll see toast messages pop up with the name of each place you tap:

Load place details

Now that you have the placeId when a user taps a POI, you can use it to look up more details about the place. The goal is to provide the user with a quick popup info window, from which they can decide if they want to bookmark the place.

To retrieve the details for places, you’ll use the Places SDK.

Before using the Places SDK, you need to create a PlacesClient. This client is your gateway to all of the available APIs provided by Places API.

In MapsActivity.kt, add the following import statements to import the places client library and the PlacesClient.

import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.net.PlacesClient

Next, add a new private member below the map member:

private lateinit var placesClient: PlacesClient

Add the following method to MapsActivity under onMapReady():

private fun setupPlacesClient() {
  Places.initialize(applicationContext, getString(R.string.google_maps_key))
  placesClient = Places.createClient(this)
}

This creates the PlacesClient. Every API call to the Places API must contain your API key. You’ll notice that you’re passing the application context to initialize the Places library. You also pass the current activity context to create the PlacesClient.

Now, add the following at the end of onCreate() to ensure PlacesClient gets initialized only once when the activity is created.

setupPlacesClient()

Next, you want to use PlacesClient to fetch details about a place. Add the following method to MapsActivity.kt:

private fun displayPoi(pointOfInterest: PointOfInterest) {
  // 1
  val placeId = pointOfInterest.placeId

  // 2
  val placeFields = listOf(Place.Field.ID,
      Place.Field.NAME,
      Place.Field.PHONE_NUMBER,
      Place.Field.PHOTO_METADATAS,
      Place.Field.ADDRESS,      
      Place.Field.LAT_LNG)

  // 3    
  val request = FetchPlaceRequest
      .builder(placeId, placeFields)
      .build()

  // 4
  placesClient.fetchPlace(request)
      .addOnSuccessListener { response ->
    // 5    
    val place = response.place
    Toast.makeText(this,
        "${place.name}, " +
        "${place.phoneNumber}",
        Toast.LENGTH_LONG).show()    
  }.addOnFailureListener { exception ->
    // 6
    if (exception is ApiException) {
      val statusCode = exception.statusCode
      Log.e(TAG,
          "Place not found: " +
          exception.message + ", " +
          "statusCode: " + statusCode)
    }
  }
}

Let’s break this down:

  1. First, you retrieve the placeId which uniquely identifies your place of interest.

  2. Next, you create a field mask that contains only the attributes of a place you are interested in retrieving. This ensures that you only request data that you use and keeps your app’s network usage under control. Notice that you’re requesting a bunch of fields that you’ll use in the latter part of this chapter.

  3. You then use these two objects to create a fetch request. You use the familiar builder pattern to create this request.

  4. Then, you fetch the place details using placesClient, which handles your request.

  5. You add a success listener which is called if the response is successfully received. You then retrieve the place object which contains the requested details. You display the name and phone number for the selected place on the screen.

  6. You also add a failure listener which catches any exception that could occur in the case the request fails. More specifically, you may want to know if there was an API error that occurred. You also log the status code and message to use for debugging the error.

Note: Many of the Places API methods like fetchPlace() make network calls, and can take a long time to return. For this reason, the places library offloads these tasks to the background and returns a Task. The network call completes asynchronously and then calls either your OnSuccessListener or OnFailureListener with a callback method.

Now, update setOnPoiClickListener() to call this new method. In onMapReady(), replace the call to map.setOnPoiClickListener() with the following:

map.setOnPoiClickListener {
  displayPoi(it)
}

This calls displayPoi() when a place on the map is tapped.

Build and run the app and tap on a few more places. This time, you’ll see the place name and its phone number, if one is available.

Note: If you don’t see the Toast pop up, check the Logcat for error messages. If you see Place not found: 9010: You have exceeded your daily request quota for this API, then check that you have enabled billing for your project in the developer console.

You have a lot of details about the place, but wouldn’t it be nice to also show a photo?

Getting a photo is not as simple as getting the basic place details, but armed with your newfound knowledge of result callbacks, you’re up to the task!

You’ll use the same callback pattern to get a photo for the selected place with a separate call to fetchPhoto. Once you retrieve the place in your success callback for fetchPlace, you can use the requested PHOTO_METADATAS field in the place object to create a FetchPhotoRequest object and subsequently make another call to the placesClient. As you can see, this code can quickly become deeply nested and messy. To avoid this, you’re going to do a bit of clean-up!

Refactoring in Android Studio

Youʼll place each main step in its own method to keep things nice and clean. You start by refactoring displayPoi() to kick off the first step. You take the code inside of displayPoi() and move it into a new method that takes a single argument. You then add a call to the new method inside displayPoi(). This is a common refactoring step that Android Studio can automate for you.

Instead of manually cutting and pasting or typing in the method call, try this:

  1. Select all of the code inside displayPoi().

  2. Press Cmd+Option+M on macOS or Ctrl-Alt-M on Windows to initiate the Extract Function command.

  3. Type in the name of the new method: displayPoiGetPlaceStep. Look at the preview window and notice that Android Studio is smart enough to add the pointOfInterest parameter that it knows you’ll need in the new method.

  4. Click OK.

Voilà! The method is created, and the call is added to displayPoi().

Your refactored code looks like this:

private fun displayPoi(pointOfInterest: PointOfInterest) {
  displayPoiGetPlaceStep(pointOfInterest)
}

private fun displayPoiGetPlaceStep(pointOfInterest: PointOfInterest) {
  val placeId = pointOfInterest.placeId

  val placeFields = listOf(Place.Field.ID,
      Place.Field.NAME,
      Place.Field.PHONE_NUMBER,
      Place.Field.PHOTO_METADATAS,
      Place.Field.ADDRESS,      
      Place.Field.LAT_LNG)

  val request = FetchPlaceRequest
      .builder(placeId, placeFields)
      .build()

  placesClient.fetchPlace(request)
      .addOnSuccessListener { response ->
    val place = response.place
    Toast.makeText(this,
        "${place.name}, " +
        "${place.phoneNumber}",
        Toast.LENGTH_LONG).show()    
  }.addOnFailureListener { exception ->
    if (exception is ApiException) {
      val statusCode = exception.statusCode
      Log.e(TAG,
          "Place not found: " +
          exception.message + ", " +
          "statusCode: " + statusCode)
    }
  }
}

Fetching a place photo

Now, you’ll add a step to retrieve a photo using the place details you requested in the previous step.

Add the following new method to MapsActivity:

private fun displayPoiGetPhotoStep(place: Place) {
  // 1
  val photoMetadata = place
      .getPhotoMetadatas()?.get(0)
  // 2    
  if (photoMetadata == null) {
    // Next step here
    return
  }            
  // 3
  val photoRequest = FetchPhotoRequest
      .builder(photoMetadata)
      .setMaxWidth(resources.getDimensionPixelSize(
          R.dimen.default_image_width))
      .setMaxHeight(resources.getDimensionPixelSize(
          R.dimen.default_image_height))
      .build()
  // 4    
  placesClient.fetchPhoto(photoRequest)
      .addOnSuccessListener { fetchPhotoResponse ->
    val bitmap = fetchPhotoResponse.bitmap
    // Next step here
  }.addOnFailureListener { exception ->
    if (exception is ApiException) {
      val statusCode = exception.statusCode
      Log.e(TAG,
          "Place not found: " +
          exception.message + ", " +
          "statusCode: " + statusCode)
    }
  }
}

You use the following steps to get a photo for the selected place:

  1. Get the first and only PhotoMetaData object from the retrieved photo metadata array for the selected place.
  2. If there’s no photo for the place, skip directly to the next step.
  3. Then, you use the builder pattern again to create the FetchPhotoRequest. You pass the builder the photoMetaData, a maximum width and a maximum height for the retrieved image.
  4. You call fetchPhoto passing in the photoRequest and let the callbacks handle the response from the Place Photos service. If the response is successfully received, assign the photo to bitmap. Otherwise, check if an ApiException occurred and log an error.

In displayPoiGetPhotoStep(), you pass a maximum height and width to get a scaled-down version of the original photo. The image is scaled proportionally to match the smaller of the two dimensions.

There are two benefits to restricting the image height and width in the photoRequest.

  1. Memory savings: In general, you never want to load photos into memory that are larger than required. Here, you limit the possibility of memory issues that can happen on lower-end devices.

  2. Bandwidth savings: Since photoRequest sends the maximum height and width in the API call, the scaling happens on the server-side and only the final scaled-down version is sent to the device.

Now, replace the Toast call in displayPoiGetPlaceStep() with a call to this new method:

  displayPoiGetPhotoStep(place)

Next, fix the unresolved references for R.dimen.default_image_width and R.dimen.default_image_height that are displayed in red. To resolve the errors, follow these steps:

  1. Place the cursor on default_image_width and press Alt-Return, then select Create a dimen value resource ‘default_image_width’.

  2. In the dialog that appears, set Resource Value to 480px and leave the other values at their defaults. Click OK.

  1. Place the cursor on default_image_height and type Alt-Return, then select Create a dimen value resource ‘default_image_height’.

  2. In the dialog that appears, set Resource Value to 270px and leave the other values at their defaults. Click OK.

This creates two values in res/values/dimens.xml for the default image width and height. You’ll see these values pop up again as you build out the app.

Note: You could have used two hard-coded numbers for the width and height parameters to getScaledPhoto(), but these are considered “magic” numbers in the code and should always be avoided.

Placing them in dimens.xml is two steps closer to coding nirvana: You gain reuse and follow the DRY principle with a single location for updating the values, as well as built-in documentation for the types of values that the numbers represent.

Add a place marker

Finally, add a step to display a marker with the place details and photo. Add the following new method to MapsActivity:

private fun displayPoiDisplayStep(place: Place, photo: Bitmap?) {
  val iconPhoto = if (photo == null) {
    BitmapDescriptorFactory.defaultMarker()
  } else {
    BitmapDescriptorFactory.fromBitmap(photo)
  }

  map.addMarker(MarkerOptions()
      .position(place.latLng as LatLng)
      .icon(iconPhoto)
      .title(place.name)
      .snippet(place.phoneNumber)
  )
}

If photo is null, you create iconPhoto as a default marker bitmap. If it’s not null, you create iconPhoto from the photo. Next, add a marker to the map by creating a new MarkerOptions object and setting the properties to the place details and the iconPhoto.

Using markers will be covered in more detail soon, but for now, it’s enough to know that addMarker() places a persistent marker on the map represented by an icon. The default marker icon is a red balloon pin but can be replaced with any bitmap image. Markers will respond to user taps and display an info window with more details.

Replace the first commented line // Next step here in displayPoiGetPhotoStep() to call your new step:

displayPoiDisplayStep(place, null)

Here, you pass along the place object and a null bitmap image.

Replace the second commented line // Next step here in displayPoiGetPhotoStep() to call your new step:

displayPoiDisplayStep(place, bitmap)

Here, you pass along the place object and the bitmap image bitmap.

Run the app and tap on some places. You should see place photos appear on the map. Tap on a photo to display an info window with the place name and phone number.

Custom info window

Now you’re making some progress! The user can tap places to view a photo and details, but having large photos all over the map is a little unwieldy. A better experience would be to display a standard marker next to each place and only show the photo and details in a popup info window.

By default, tapping on a marker displays a standard info window. This window looks like the following:

The standard info window will display the title and snippet as defined on the marker. If you want to display additional information, a custom info window is in order.

InfoWindowAdapter class

To create a custom info window, you create a class that conforms to the InfoWindowAdapter interface and then call map.setInfoWindowAdapter() with an instance of the class.

There are two methods to implement in InfoWindowAdapter:

  1. getInfoWindow(): This one allows you to return a custom view for the full info window.

  2. getInfoContents(): This allows you to return a custom view for the interior contents of the info window only without changing the default outer window and background.

In your case, only the info window contents will be replaced. Before creating a custom info window, you need to create a layout file for the contents. The layout will look like this:

Create a new layout resource file named res/layout/content_bookmark_info.xml with the following contents:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="5dp">

  <ImageView
      android:id="@+id/photo"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginEnd="5dp"
      android:adjustViewBounds="true"
      android:maxWidth="200dp"
      android:scaleType="fitStart"
      android:contentDescription="@string/bookmark_image"
      android:src="@drawable/default_photo"/>

  <LinearLayout
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:orientation="vertical">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:textColor="#ff000000"
        android:textSize="14sp"
        android:textStyle="bold"
        tools:text="Place Title"/>

    <TextView
        android:id="@+id/phone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="#ff7f7f7f"
        android:textSize="12sp"
        tools:text="555-121-1212"/>

  </LinearLayout>
</LinearLayout>

You use a horizontal LinearLayout to wrap the place image and another vertical LinearLayout for the place details. You’ll load this Layout from InfoWindowAdapter and populate the ImageView and both TextViews.

Open res/values/strings.xml and add the following content description string for the bookmark image view:

<string name="bookmark_image">Bookmark image</string>

Before you jump into creating the class that conforms to the InfoWindowAdapter interface, you will add View Binding to Placebook. Using View Binding will help you to easily write code that interacts with views in your app and make the compilation of your app faster. Each XML layout file gets a corresponding automatically generated binding class, which has references to all views with an id in the layout file.

Open app/build.gradle and add the following below kotlinOptions:

buildFeatures {
  viewBinding true
}

Now, sync the project so Android Studio registers the change in your gradle file. This generates a binding class called ContentBookmarkInfoBinding, which is basically the name of the XML layout file in Pascal case with the Binding suffix. ContentBookmarkInfoBinding contains references to the root view, the top-level Linear Layout you added in content_bookmark_info.xml, and all views that have an id.

Now, create a new package named adapter and then a new Kotlin class named BookmarkInfoWindowAdapter.kt within the adapter package.

BookmarkInfoWindowAdapter is your custom InfoWindowAdapter.

Replace the contents of BookmarkInfoWindowAdapter.kt with the following:

// 1
package com.raywenderlich.placebook.adapter

import com.raywenderlich.placebook.databinding.ContentBookmarkInfoBinding

// 2
class BookmarkInfoWindowAdapter(context: Activity) : GoogleMap.InfoWindowAdapter {

  // 3
  private val binding = ContentBookmarkInfoBinding.inflate(context.layoutInflater)

  // 4
  override fun getInfoWindow(marker: Marker): View? {
    // This function is required, but can return null if
    // not replacing the entire info window
    return null
  }

  // 5
  override fun getInfoContents(marker: Marker): View? {
    binding.title.text = marker.title ?: ""
    binding.phone.text = marker.snippet ?: ""
    return binding.root
  }
}

Here’s what’s happening:

  1. You declare the package and add an import for the auto-generated binding class ContentBookmarkInfoBinding.

  2. You declare BookmarkInfoWindowAdapter to take a single parameter representing the hosting activity. The class implements the GoogleMap.InfoWindowAdapter interface.

  3. You initialize the variable binding by calling the static inflate method which creates an instance of the binding class.

  4. You override getInfoWindow() and return null to indicate that you won’t be replacing the entire info window.

  5. You override getInfoContents() and fill in the title and phone TextViews on the Layout.

Once this object is assigned, the map will call getInfoWindow() whenever it needs to display an info window for a particular marker.

Note that you’re not providing an image for the ImageView at this point. The only information you’re given in getInfoContents() is the associated Marker, and it doesn’t store the photo. This will be fixed soon, but for now, you’ll continue to hook up the window adapter.

Assigning the InfoWindowAdapter

In MapsActivity.kt, add the following line to onMapReady() after map is assigned:

map.setInfoWindowAdapter(BookmarkInfoWindowAdapter(this))

Here, you assign your custom InfoWindowAdapter to map.

You no longer need to set the photo as the marker icon. In displayPoiDisplayStep(), remove the lines that create the iconPhoto variable. Then remove setIcon() from MarkerOptions. The entire body of displayPoiDisplayStep() should look like this:

 map.addMarker(MarkerOptions()
     .position(place.latLng as LatLng)
     .title(place.name)
     .snippet(place.phoneNumber)
 )

Run the app and tap on any place. A default red balloon marker will be added. Tap on the marker to display the info window.

You’ll finish off the BookmarkInfoWindowAdapter by adding the place image.

Marker tags

So, how do you associate the image with the marker? There are several ways to tackle this problem, but they all involve using the tag property of the Marker object.

Marker provides the tag property as a means to associate the marker with data you are managing in the app. This could be a simple index into a list or dictionary, a full complex object, or in this case, a Bitmap object.

In displayPoiDisplayStep(), replace the call to addMarker() with this:

val marker = map.addMarker(MarkerOptions()
    .position(place.latLng as LatLng)
    .title(place.name)
    .snippet(place.phoneNumber)
)
marker?.tag = photo

Here, addMarker() returns a Marker object and you assign it to marker. You then assign photo to the tag property.

Next, add the following lines to getInfoContents() in BookmarkInfoWindowAdapter.kt before the return line:

val imageView = binding.photo
imageView.setImageBitmap((marker.tag as Bitmap))

Since you assigned the place’s image bitmap with the marker’s tag property, when the map draws the info window contents it can set the ImageView to display the photo.

Run the app and tap on a place and the marker. This time the place photo will display in the info window.

Key Points

Maps are only useful when they offer the right amount of information. Google Places API can help you to enrich your map-based app with information about places. In this chapter you learned:

  • How to set up Places API in your Google console account.
  • How to add Places API to your app and use it for selecting points of interest from your map.
  • How to retrieve information about a specific point of interest using Places API.
  • How to display information on your map using InfoWindowAdapter.

Where to go from here?

Pat yourself on the back for making it this far! You have everything you need to move on to the bookmarking feature.

In the next chapter, you’ll learn how to save places to a local database and let the user edit place details.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.