Place Details component

The Place Details component of the Places UI Kit lets you add an individual UI component that displays place details in your app.

Place details compact component

The PlaceDetailsCompactFragment renders details for a selected place using minimal space. This may be useful in an info window highlighting a place on a map, in a social media experience like sharing a location in a chat, as a suggestion for selecting your current location, or within a media article to reference the place on Google Maps. The PlaceDetailsCompactFragment can display name, address, rating, type, price, accessibility icon, open status, and a single photo.

The Place Details component can be used independently or in conjunction with other Google Maps Platform APIs and services. The component takes either a Place ID or latitude/longitude coordinates and returns rendered Place Details information.

Billing

When using the Place Details UI Kit, you are billed for each time the .loadWithPlaceId() or the .loadWithResourceName() method is called. If you load the same place multiple times, you are billed for each request.

To avoid being charged multiple times, don't directly add .loadWithPlaceId() or .loadWithResourceName()in Android lifecycle methods. For example, don't directly call .loadWithPlaceId() or .loadWithResourceName()in the onResume() method.

Add place details to your app

You can add place details to your app by adding a fragment to a layout. When you instantiate the fragment, you can customize the look and feel of the place details information to suit your needs and match your app's appearance.

You have two methods available in both Kotlin and Java: one to load the fragment with a Place ID (loadWithPlaceId()) and one to load the fragment with a resource name (loadWithResourceName()). You can choose either method, or both if you are using both place IDs and resource names.

You can specify orientation (horizontal or vertical), theme overrides, and content. The content options are media, address, rating, price, type, accessible entrance, and open now status. Learn more about customization.

Kotlin

              
        // Create a new instance of the fragment from the Places SDK.
        val fragment = PlaceDetailsCompactFragment.newInstance(
            PlaceDetailsCompactFragment.ALL_CONTENT,
            orientation,
            R.style.CustomizedPlaceDetailsTheme,
        ).apply {
            // Set a listener to be notified when the place data has been loaded.
            setPlaceLoadListener(object : PlaceLoadListener {
                override fun onSuccess(place: Place) {
                    Log.d(TAG, "Place loaded: ${place.id}")
                    // Hide loader, show the fragment container and the dismiss button
                    binding.loadingIndicator.visibility = View.GONE
                    binding.placeDetailsContainer.visibility = View.VISIBLE
                    binding.dismissButton.visibility = View.VISIBLE
                }

                override fun onFailure(e: Exception) {
                    Log.e(TAG, "Place failed to load", e)
                    // Hide everything on failure
                    dismissPlaceDetails()
                    Toast.makeText(this@MainActivity, "Failed to load place details.", Toast.LENGTH_SHORT).show()
                }
            })
        }

        // Add the fragment to the container in the layout.
        supportFragmentManager
            .beginTransaction()
            .replace(binding.placeDetailsContainer.id, fragment)
            .commitNow() // Use commitNow to ensure the fragment is immediately available.

        // **This is the key step**: Tell the fragment to load data for the given Place ID.
        binding.root.post {
            fragment.loadWithPlaceId(placeId)
        }
    }

Java

      
PlaceDetailsCompactFragment fragment =
  PlaceDetailsCompactFragment.newInstance(
        Orientation.HORIZONTAL,
        Arrays.asList(Content.ADDRESS, Content.TYPE, Content.RATING, Content.ACCESSIBLE_ENTRANCE_ICON),
        R.style.CustomizedPlaceDetailsTheme);
    
fragment.setPlaceLoadListener(
  new PlaceLoadListener() {
        @Override public void onSuccess(Place place) { ... }
    
        @Override public void onFailure(Exception e) { ... }
});
    
getSupportFragmentManager()
      .beginTransaction()
      .add(R.id.fragment_container, fragment)
      .commitNow();
    
// Load the fragment with a Place ID.
fragment.loadWithPlaceId(placeId);
      
// Load the fragment with a resource name.
// fragment.loadWithResourceName(resourceName);

See complete code to load the Place Details component

Kotlin

        
package com.example.placedetailsuikit

import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.location.Location
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.lifecycle.ViewModel
import com.example.placedetailsuikit.databinding.ActivityMainBinding
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.PointOfInterest
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.widget.PlaceDetailsCompactFragment
import com.google.android.libraries.places.widget.PlaceLoadListener
import com.google.android.libraries.places.widget.model.Orientation

private const val TAG = "PlacesUiKit"

/**
 * A simple ViewModel to store UI state that needs to survive configuration changes.
 * In this case, it holds the ID of the selected place.
 */
class MainViewModel : ViewModel() {
    var selectedPlaceId: String? = null
}

/**
 * Main Activity for the application. This class is responsible for:
 * 1. Displaying a Google Map.
 * 2. Handling location permissions to center the map on the user's location.
 * 3. Handling clicks on Points of Interest (POIs) on the map.
 * 4. Displaying a [PlaceDetailsCompactFragment] to show details of a selected POI.
 */
class MainActivity : AppCompatActivity(), OnMapReadyCallback, GoogleMap.OnPoiClickListener {
    // ViewBinding for safe and easy access to views.
    private lateinit var binding: ActivityMainBinding
    private var googleMap: GoogleMap? = null

    // Client for retrieving the device's last known location.
    private lateinit var fusedLocationClient: FusedLocationProviderClient

    // Modern approach for handling permission requests and their results.
    private lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>>

    // ViewModel to store state across configuration changes (like screen rotation).
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialize the permissions launcher. This defines what to do after the user
        // responds to the permission request dialog.
        requestPermissionLauncher =
            registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
                if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true || permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true) {
                    // Permission was granted. Fetch the user's location.
                    Log.d(TAG, "Location permission granted by user.")
                    fetchLastLocation()
                } else {
                    // Permission was denied. Show a message and default to a fallback location.
                    Log.d(TAG, "Location permission denied by user.")
                    Toast.makeText(
                        this,
                        "Location permission denied. Showing default location.",
                        Toast.LENGTH_LONG
                    ).show()
                    moveToSydney()
                }
            }

        // Standard setup for ViewBinding and enabling edge-to-edge display.
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Set up the dismiss button listener
        binding.dismissButton.setOnClickListener {
            dismissPlaceDetails()
        }

        // --- Crucial: Initialize Places SDK ---
        val apiKey = BuildConfig.PLACES_API_KEY
        if (apiKey.isEmpty() || apiKey == "YOUR_API_KEY") {
            Log.e(TAG, "No api key")
            Toast.makeText(
                this,
                "Add your own API_KEY in local.properties",
                Toast.LENGTH_LONG
            ).show()
            finish()
            return
        }

        // Initialize the SDK with the application context and API key.
        Places.initializeWithNewPlacesApiEnabled(applicationContext, apiKey)

        // Initialize the location client.
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        // ------------------------------------

        // Obtain the SupportMapFragment and request the map asynchronously.
        val mapFragment =
            supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment?
        mapFragment?.getMapAsync(this)

        // After rotation, check if a place was selected. If so, restore the fragment.
        if (viewModel.selectedPlaceId != null) {
            viewModel.selectedPlaceId?.let { placeId ->
                Log.d(TAG, "Restoring PlaceDetailsFragment for place ID: $placeId")
                showPlaceDetailsFragment(placeId)
            }
        }
    }

    /**
     * Callback triggered when the map is ready to be used.
     */
    override fun onMapReady(map: GoogleMap) {
        Log.d(TAG, "Map is ready")
        googleMap = map
        // Set a listener for clicks on Points of Interest.
        googleMap?.setOnPoiClickListener(this)

        // Check for location permissions to determine the initial map position.
        if (isLocationPermissionGranted()) {
            fetchLastLocation()
        } else {
            requestLocationPermissions()
        }
    }

    /**
     * Checks if either fine or coarse location permission has been granted.
     */
    private fun isLocationPermissionGranted(): Boolean {
        return ActivityCompat.checkSelfPermission(
            this,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.ACCESS_COARSE_LOCATION
                ) == PackageManager.PERMISSION_GRANTED
    }

    /**
     * Launches the permission request flow. The result is handled by the
     * ActivityResultLauncher defined in onCreate.
     */
    private fun requestLocationPermissions() {
        Log.d(TAG, "Requesting location permissions.")
        requestPermissionLauncher.launch(
            arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
            )
        )
    }

    /**
     * Fetches the device's last known location and moves the map camera to it.
     * This function should only be called after verifying permissions.
     */
    @SuppressLint("MissingPermission")
    private fun fetchLastLocation() {
        if (isLocationPermissionGranted()) {
            fusedLocationClient.lastLocation
                .addOnSuccessListener { location: Location? ->
                    if (location != null) {
                        // Move camera to user's location if available.
                        val userLocation = LatLng(location.latitude, location.longitude)
                        googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(userLocation, 13f))
                        Log.d(TAG, "Moved to user's last known location.")
                    } else {
                        // Fallback to a default location if the last location is null.
                        Log.d(TAG, "Last known location is null. Falling back to Sydney.")
                        moveToSydney()
                    }
                }
                .addOnFailureListener {
                    // Handle errors in fetching location.
                    Log.e(TAG, "Failed to get location.", it)
                    moveToSydney()
                }
        }
    }

    /**
     * A default fallback location for the map camera.
     */
    private fun moveToSydney() {
        val sydney = LatLng(-33.8688, 151.2093)
        googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(sydney, 13f))
        Log.d(TAG, "Moved to Sydney")
    }

    /**
     * Callback for when a Point of Interest on the map is clicked.
     */
    override fun onPoiClick(poi: PointOfInterest) {
        val placeId = poi.placeId
        Log.d(TAG, "Place ID: $placeId")

        // Save the selected place ID to the ViewModel to survive rotation.
        viewModel.selectedPlaceId = placeId
        showPlaceDetailsFragment(placeId)
    }

    /**
     * Instantiates and displays the [PlaceDetailsCompactFragment].
     * @param placeId The unique identifier for the place to be displayed.
     */
    private fun showPlaceDetailsFragment(placeId: String) {
        Log.d(TAG, "Showing PlaceDetailsFragment for place ID: $placeId")

        // Show the wrapper, hide the dismiss button, and show the loading indicator.
        binding.placeDetailsWrapper.visibility = View.VISIBLE
        binding.dismissButton.visibility = View.GONE
        binding.placeDetailsContainer.visibility = View.GONE
        binding.loadingIndicator.visibility = View.VISIBLE

        // Determine the orientation based on the device's current configuration.
        val orientation =
            if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
                Orientation.HORIZONTAL
            } else {
                Orientation.VERTICAL
            }

        
        // Create a new instance of the fragment from the Places SDK.
        val fragment = PlaceDetailsCompactFragment.newInstance(
            PlaceDetailsCompactFragment.ALL_CONTENT,
            orientation,
            R.style.CustomizedPlaceDetailsTheme,
        ).apply {
            // Set a listener to be notified when the place data has been loaded.
            setPlaceLoadListener(object : PlaceLoadListener {
                override fun onSuccess(place: Place) {
                    Log.d(TAG, "Place loaded: ${place.id}")
                    // Hide loader, show the fragment container and the dismiss button
                    binding.loadingIndicator.visibility = View.GONE
                    binding.placeDetailsContainer.visibility = View.VISIBLE
                    binding.dismissButton.visibility = View.VISIBLE
                }

                override fun onFailure(e: Exception) {
                    Log.e(TAG, "Place failed to load", e)
                    // Hide everything on failure
                    dismissPlaceDetails()
                    Toast.makeText(this@MainActivity, "Failed to load place details.", Toast.LENGTH_SHORT).show()
                }
            })
        }

        // Add the fragment to the container in the layout.
        supportFragmentManager
            .beginTransaction()
            .replace(binding.placeDetailsContainer.id, fragment)
            .commitNow() // Use commitNow to ensure the fragment is immediately available.

        // **This is the key step**: Tell the fragment to load data for the given Place ID.
        binding.root.post {
            fragment.loadWithPlaceId(placeId)
        }
    }


    private fun dismissPlaceDetails() {
        binding.placeDetailsWrapper.visibility = View.GONE
        viewModel.selectedPlaceId = null
    }

    override fun onDestroy() {
        super.onDestroy()
        // Clear references to avoid memory leaks.
        googleMap = null
    }
}
        
Tip: Access the full code sample on GitHub.

Customize place details

Places UI kit offers a design system approach to visual customization roughly based on Material Design (with some Google-Maps-specific modifications). See Material Design's reference for Color and Typography. By default, the style adheres to the Google Maps visual design language.

Place details customization options

When instantiating a fragment, you can specify a theme that overrides any of the default style attributes. Any theme attributes that are not overridden use the default styles. If you'd like to support a dark theme, you can add an entry for the color in values-night/colors.xml.

  <style name="CustomizedPlaceDetailsTheme" parent="PlacesMaterialTheme">
    <item name="placesColorPrimary">@color/app_primary_color</item>
    <item name="placesColorOnSurface">@color/app_color_on_surface</item>
    <item name="placesColorOnSurfaceVariant">@color/app_color_on_surface</item>
  
    <item name="placesTextAppearanceBodySmall">@style/app_text_appearence_small</item>
  
    <item name="placesCornerRadius">20dp</item>
  </style>

You can customize the following styles:

Theme attribute Usage
Color
placesColorSurface Container and dialog background
placesColorOnSurface Headings, dialog content
placesColorOnSurfaceVariant Place information
placesColorPrimary Links
placesColorOutlineDecorative Container border
placesColorSecondaryContainer Button background
placesColorOnSecondaryContainer Button text and icon
placesColorPositive Place "Open" now label
placesColorNegative Place "Closed" now label
placesColorInfo Accessible entrance icon
   
Typography
placesTextAppearanceHeadlineMedium Dialog headings
placesTextAppearanceTitleSmall Place name
placesTextAppearanceBodyMedium Dialog content
placesTextAppearanceBodySmall Place information
placesTextAppearanceLabelLarge Button label
   
Corners
placesCornerRadius Container corners
   
Google Maps Brand Attribution
placesColorAttributionLight Light theme Google Maps attribution and disclosure button (enums for white, gray, and black)
placesColorAttributionDark Dark theme Google Maps attribution and disclosure button (enums for white, gray, and black)

Width and height

For vertical views, the recommended width is between 180dp and 300dp. For horizontal views, the recommended width is between 180dp and 500dp. View smaller than 160dp may not display correctly.

Best practice is to not set a height. This will allow the content in the window to set the height, allowing all the information to be displayed.

Attribution colors

Google Maps' terms of service require you to use one of three brand colors for the Google Maps attribution. This attribution must be visible and accessible when customization changes have been made.

We offer 3 brand colors to choose from that can be independently set for light and dark themes:

  • Light theme: placesColorAttributionLight with enums for white, gray, and black.
  • Dark theme: placesColorAttributionDark with enums for white, gray, and black.

Customization examples

This sample customizes the standard content.

  val fragmentStandardContent = PlaceDetailsCompactFragment.newInstance(
    PlaceDetailsCompactFragment.STANDARD_CONTENT,
    orientation,
    R.style.CustomizedPlaceDetailsTheme
  )

This sample customizes content options.

  val placeDetailsFragment = PlaceDetailsCompactFragment.newInstance(
    orientation,
    listOf(
        Content.ADDRESS,
        Content.ACCESSIBLE_ENTRANCE,Content.MEDIA
    ),
    R.style.CustomizedPlaceDetailsTheme
)
  

This sample customizes all Content options.

  val fragmentAllContent = PlaceDetailsCompactFragment.newInstance(
    orientation,
    PlaceDetailsCompactFragment.ALL_CONTENT,
    R.style.CustomizedPlaceDetailsTheme
  )