alpaca0984.log

Google — Place Autocompletion API

alpaca0984

If you work on an application that requires your users to fill out their addresses, there is an SDK that you should consider using. It is the Google's Place Autocomplete SDK. It enables your users to select a place from the prediction list, instead of typing everything by themselves. It must enhance your app's user experience.

In this post, I will show you how to integrate the SDK in your Android app using Hilt and Jetpack Compose. The full codebase can be found on my Github repository.


Install dependencies

In your project build.gradle add listed plugins.

plugins {
    id 'com.android.application' version '7.4.1' apply false
    id 'com.android.library' version '7.4.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
 
    // Add these libraries
    id 'com.google.dagger.hilt.android' version '2.44' apply false
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' apply false
}

Also, modify your app-level build file (apps/build.gradle).

plugins {
    id 'com.google.dagger.hilt.android'
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
 
kapt {
    correctErrorTypes true
}
 
dependencies {
    // Hilt
    implementation "com.google.dagger:hilt-android:2.44"
    kapt "com.google.dagger:hilt-compiler:2.44"
 
    // Places SDK
    implementation 'com.google.android.libraries.places:places:3.0.0'
}

Set Google Maps API Key

Open your local.properties and set a key:

MAPS_API_KEY=<YOUR_API_KEY>

And set the key as meta-data in a manifest (src/main/AndroidManifest.xml).

<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="${MAPS_API_KEY}" />

Moreover, update your apps/build.gradle to load the API key and set it to BuildConfig. Later you will need to retrieve it to instantiate the Places SDK.

android {
 
    defaultConfig {
        Properties properties = new Properties()
        properties.load(project.rootProject.file("local.properties").newDataInputStream())
        buildConfigField("String", "MAPS_API_KEY", properties.getProperty("MAPS_API_KEY"))
    }
}

Initialize Places SDK

Places SDK needs to be initialized before being accessed. You can do it in onCreate() of your application class.

@HiltAndroidApp
class MyApplication : Application() {
 
    override fun onCreate() {
        super.onCreate()
 
        Places.initialize(applicationContext, BuildConfig.MAPS_API_KEY)
    }
}

Instantiate Places Client and provides it

Create a Hilt module to provide PlacesClient. It requires an Android Context so it needs to be installed into SingletonComponent.

@Module
@InstallIn(SingletonComponent::class)
class ApplicationModule {
 
    @Provides
    fun placesClient(application: Application): PlacesClient = Places.createClient(application)
}

Create a View Model and communicate with Places SDK

First, let's create models that represent UI states.

data class PlacesState(
    val query: String = "",
    val predictions: List<AutocompletePrediction> = emptyList(),
    val addressDetails: AddressDetails? = null,
)
 
data class AddressDetails(
    val streetNumber: AddressComponent?,
    val route: AddressComponent?,
    val postalCode: AddressComponent?,
    val country: AddressComponent?,
)

Then we can use those models inside a View Model. The AutocompleteSessionToken groups the query and selection phases of a user search into a discrete session for billing purposes. The session begins when the user starts typing a query and concludes when they select a place. The detail is here.

private const val TAG = "PlaceAutocompletionViewModel"
 
@HiltViewModel
class MainViewModel @Inject constructor(
    private val placesClient: PlacesClient,
) : ViewModel() {
 
    private val _placesState = MutableStateFlow(PlacesState())
    val placesState: StateFlow<PlacesState> = _placesState.asStateFlow()
 
    private var token = AutocompleteSessionToken.newInstance()
 
    /**
     * Find place predictions based on the [query].
     */
    fun predictPlace(query: String) {
        val request = FindAutocompletePredictionsRequest.builder()
            // Filter results in a specific country. You can dynamically change it
            // depdending on where the user lives.
            .setCountries("DE")
            .setSessionToken(token)
            .setQuery(query)
            .build()
 
        placesClient.findAutocompletePredictions(request)
            .addOnSuccessListener { response ->
                _placesState.value = _placesState.value.copy(
                    query = query,
                    predictions = response.autocompletePredictions,
                )
            }
            .addOnFailureListener { exception: Exception? ->
                if (exception is ApiException) {
                    Log.e(TAG, "Place not found: " + exception.message)
                }
            }
    }
 
    /**
     * Fetch place data from [placeId].
     */
    fun fetchPlace(placeId: String) {
        val placeFields = listOf(Place.Field.ADDRESS_COMPONENTS)
        val request = FetchPlaceRequest.newInstance(placeId, placeFields)
        placesClient.fetchPlace(request)
            .addOnSuccessListener { response ->
                _placesState.value = _placesState.value.copy(
                    addressDetails = response.retrieveAddressDetails(),
                )
                token = AutocompleteSessionToken.newInstance()
            }
            .addOnFailureListener { exception: Exception ->
                if (exception is ApiException) {
                    Log.e(TAG, "Place not found: " + exception.message)
                }
            }
    }
}
 
/**
 * Convert `FetchPlaceResponse` to our UI model to let our screen easily accesses to
 * fetched place details.
 */
private fun FetchPlaceResponse.retrieveAddressDetails(): AddressDetails? =
    place.addressComponents?.asList()?.let { components ->
        AddressDetails(
            streetNumber = components.find { component ->
                component.types.any { it == "street_number" }
            },
            route = components.find { component ->
                component.types.any { it == "route" }
            },
            postalCode = components.find { component ->
                component.types.any { it == "postal_code" }
            },
            country = components.find { component ->
                component.types.any { it == "country" }
            },
        )
    }

Create Screen

Finally, we build a simple UI where you can fill out an address and show predictions.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
 
    private val viewModel: MainViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PlaceAutocompletionSampleTheme {
                MainScreen(viewModel = viewModel)
            }
        }
    }
}
 
@Composable
private fun MainScreen(viewModel: MainViewModel) {
    val state by viewModel.placesState.collectAsState()
 
    Surface(modifier = Modifier.fillMaxSize()) {
        Column(
            modifier = Modifier.padding(16.dp),
        ) {
            // A text field where the user types their address
            OutlinedTextField(
                value = state.query,
                onValueChange = {
                    // when text is changed, query predictions to Places SDK
                    viewModel.predictPlace(query = it)
                },
                modifier = Modifier.fillMaxWidth(),
            )
 
            // Show predictions
            if (state.predictions.isNotEmpty()) {
                LazyColumn(
                    modifier = Modifier
                        .fillMaxWidth()
                        .border(width = 1.dp, color = Color.Gray)
                        .padding(12.dp),
                ) {
                    itemsIndexed(state.predictions) { index, prediction ->
                        if (index > 0)
                            Divider()
 
                        Column(
                            modifier = Modifier
                                .padding(vertical = 8.dp)
                                .clickable {
                                    // When a prediction is selected, fetch Place detail information
                                    // from Places SDK
                                    viewModel.fetchPlace(placeId = prediction.placeId)
                                },
                        ) {
                            Text(
                                text = prediction.getPrimaryText(null).toString(),
                                style = MaterialTheme.typography.body2,
                            )
 
                            Text(
                                text = prediction.getSecondaryText(null).toString(),
                                style = MaterialTheme.typography.caption,
                            )
                        }
                    }
                }
            }
 
            // Show place details when one of the predictions is selected
            state.addressDetails?.let { details ->
                Spacer(Modifier.height(16.dp))
                Column(
                    modifier = Modifier.fillMaxWidth(),
                    verticalArrangement = Arrangement.spacedBy(8.dp),
                ) {
                    Text(text = "Street Number: ${details.streetNumber?.name}")
                    Text(text = "Street: ${details.route?.name}")
                    Text(text = "Postal Code: ${details.postalCode?.name}")
                    Text(text = "Country: ${details.country?.name}")
                }
            }
        }
    }
}

That's all. Here is a screencast showing how it works. I tried it somewhere in Berlin, Germany, because it's where I live but the SDK works worldwide.

The Places SDK is very powerful and I believe it improves your app's UX in many cases. So, unsurprisingly, it's not for free. Calling findAutocompletePredictions() costs a fee per session until you call fetchPlace() (within a few minutes of the beginning of the querying).

price-table

For price details, you can find them on Places API Usage and Billing.