# Integrating Google Maps, Places API, and Reverse Geocoding with Jetpack Compose

<iframe src="https://androidweekly.net/issues/issue-553/badge" height="25px"></iframe>

Integrating Google maps into Android apps has been somewhat of a tough nut to crack for developers. With the advent of Jetpack Compose for building native UI on Android, the Google Maps team announced a Map SDK for Compose, available [here](https://github.com/googlemaps/android-maps-compose). However, it is only a piece of the puzzle.

The SDK helps with drawing the map on the screen and gives you a very featureful `GoogleMap` composable with tons of customization, but if you are looking to build a full-fledged address selection screen akin to Ola, Uber, or Swiggy, you may need to integrate several APIs in addition to this.

The two most important are:

1. [Places API](https://developers.google.com/maps/documentation/places/android-sdk/overview), used to facilitate **autocomplete** and provide details for a place on the map.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673193812807/db423776-b40a-4ee3-bed8-5955161e4160.png align="center")
    
2. [Geocoding API](https://developer.android.com/reference/android/location/Geocoder), used to convert between lat-long coordinates and the textual address for a point.
    

Let's walk through their implementation.

# Our Goal

We will build a Map screen where users can select their current location. They can do so either by moving the marker to their desired location or by searching for a place in the `TextField`. The finished screen will look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673195316045/128a1b07-3192-4083-aa30-8063291f8ba5.jpeg align="center")

Let's get started.

## 1\. Google Maps Platform

To use the Google Maps platform, you must create a Google Cloud project, link it to a billing account, and enable the Google Maps SDK for Android.

> At the time of writing, Google Maps provides all projects with $200 credit that recurs monthly. That means that you likely won't be paying anything until you have a lot of users. Your billing account will only be charged if you spend more than $200 in a month.

To do so, follow these steps:

1. Create a Google Cloud Project and/or enable **Maps SDK for Android, Places API,** and **Geocoding** API. Extensive instructions and direct links are available [here](https://developers.google.com/maps/documentation/android-sdk/cloud-setup)
    
2. Obtain an API key by following the instructions [here](https://developers.google.com/maps/documentation/android-sdk/get-api-key). This API key will be used to authenticate your app with the Google Maps service.
    
    > **It's recommended to restrict the API key to your app by following the instructions in the** [**doc**](https://developers.google.com/maps/documentation/android-sdk/get-api-key#restrict_key)**. This prevents misuse of your API key if it's leaked.**
    
3. Set up your app to use the Google Maps platform, and specify the API key in the manifest. Instructions [here](https://developers.google.com/maps/documentation/android-sdk/config#step_2_set_up_the_sdk).
    
4. By now, you should've added your API key to your `local.properties` name `MAPS_API_KEY`. We'll need the key later for interacting with the Places API, so create a buildConfigField for it. In your app-level `build.gradle`, add the following code inside the `defaultConfig` block:
    
    ```java
    Properties properties = new Properties()
    if (rootProject.file("local.properties").exists()) {
        properties.load(
                rootProject
                .file("local.properties")
                .newDataInputStream()
        )
    }
    
    buildConfigField(
        "String",
        "MAPS_API_KEY",
        properties.getProperty("MAPS_API_KEY")
    )
    ```
    
    This will create a new BuildConfig field from the key that you have defined in your `local.properties` file, which can be used anywhere in the app by referencing `BuildConfig.MAPS_API_KEY`.
    

## 2\. GoogleMap Composable

To display a Google Map in your Compose-based Android app, you can use the [SDK for compose](https://github.com/googlemaps/android-maps-compose). It wraps an AndroidView under the hood and gives you several good abstractions that'll help you write idiomatic Compose code. Instructions on integrating the SDK are provided via the [README](https://github.com/googlemaps/android-maps-compose/blob/main/README.md) of the project.

We want our map to initially be focused on the user's current location. To obtain the current location, we'll need

1. `ACCESS_FINE_LOCATION` permission
    
2. Location enabled in the settings
    

To represent all these states, we can construct a sealed class:

```kotlin
sealed class LocationState {
    object NoPermission: LocationState()
    object LocationDisabled: LocationState()
    object LocationLoading: LocationState()
    data class LocationAvailable(val location: LatLng): LocationState()
    object Error: LocationState()
}
```

This is an extremely simplified version. A production app will have more complex state interactions.

Now, we'll build our `ViewModel` for the screen, which will act as the state holder, containing an instance of the above-defined sealed class. It'll also be responsible for fetching and updating the current location. To do so, we'll use the `FusedLocationProviderClient`. You can read more about it [here](https://developer.android.com/training/location/retrieve-current). Currently, our `ViewModel` looks like this:

```kotlin
class LocationViewModel : ViewModel() {
    lateinit var fusedLocationClient: FusedLocationProviderClient

    var locationState by mutableStateOf<LocationState>(LocationState.NoPermission)

    @SuppressLint("MissingPermission")
    fun getCurrentLocation() {
        locationState = LocationState.LocationLoading
        fusedLocationClient
            .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
            .addOnSuccessListener { location ->
                locationState = if (location == null && locationState !is LocationState.LocationAvailable) {
                    LocationState.Error
                } else {
                    LocationState.LocationAvailable(LatLng(location.latitude, location.longitude))
                }
            }
    }
}
```

Note that the `fusedLocationClient` is a `lateinit` object, so we'll need to instantiate it in the fragment's `onCreate`:

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

    viewModel.fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity())
}
```

Now, let's define a couple of self-explanatory utility functions in the fragment (they can also be made extension functions or stored separately in a util file with the activity being an argument):

```kotlin
private fun requestLocationEnable() {
    activity?.let {
        val locationRequest = LocationRequest.create()
        val builder = LocationSettingsRequest
            .Builder()
            .addLocationRequest(locationRequest)
        val task = LocationServices
            .getSettingsClient(it)
            .checkLocationSettings(builder.build())
            .addOnSuccessListener {
                if (it.locationSettingsStates?.isLocationPresent == true) {
                    viewModel.getCurrentLocation()
                }
            }
            .addOnFailureListener {
                if (it is ResolvableApiException) {
                    try {
                        it.startResolutionForResult(requireActivity(), 999)
                    } catch (e : IntentSender.SendIntentException) {
                        e.printStackTrace()
                    }
                }
            }

    }
}

private fun locationEnabled(): Boolean {
    val locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager
    return LocationManagerCompat.isLocationEnabled(locationManager)
}
```

With our states and utility methods defined, we finally move to our composable. We can use the [Permissions](https://google.github.io/accompanist/permissions/) Library from the Accompanist collection to request location permission.

```kotlin
val locationPermissionState = rememberMultiplePermissionsState(
    listOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
    )
)
```

Then we can add a `LaunchedEffect` to update the state when the permission state changes. If they are granted, we check for the location is enabled, and if that's also enabled, we ask the `ViewModel` to get the current location.

```kotlin
LaunchedEffect(locationPermissionState.allPermissionsGranted) {
    if (locationPermissionState.allPermissionsGranted) {
        if (locationEnabled()) {
            viewModel.getCurrentLocation()
        } else {
            viewModel.locationState = LocationState.LocationDisabled
        }
    }
}
```

Lastly, we define what UI will be rendered on basis of the location state:

```kotlin
AnimatedContent(
    viewModel.locationState
) { state ->
    when (state) {
        is LocationState.NoPermission -> {
            Column {
                Text("We need location permission to continue")
                Button(onClick = { locationPermissionState.launchMultiplePermissionRequest() }) {
                    Text("Request permission")
                }
            }
        }
        is LocationState.LocationDisabled -> {
            Column {
                Text("We need location to continue")
                Button(onClick = { requestLocationEnable() }) {
                    Text("Enable location")
                }
            }
        }
        is LocationState.LocationLoading -> {
            Text("Loading Map")
        }
        is LocationState.Error -> {
            Column {
                Text("Error fetching your location")
                Button(onClick = { viewModel.getCurrentLocation() }) {
                    Text("Retry")
                }
            }
        }
        is LocationState.LocationAvailable -> {
            val cameraPositionState = rememberCameraPositionState {
                position = CameraPosition.fromLatLngZoom(state.location, 15f)
            }
            val mapUiSettings by remember { mutableStateOf(MapUiSettings()) }
            val mapProperties by remember { mutableStateOf(MapProperties(isMyLocationEnabled = true)) }

            GoogleMap(
                modifier = Modifier.fillMaxSize(),
                cameraPositionState = cameraPositionState,
                uiSettings = mapUiSettings,
                properties = mapProperties
            )
        }
    }
}
```

In the `GoogleMap` composable, we can see that it fills the entire screen and takes a Camera Position State, which is responsible for what the user sees on the screen. In our code, the position is set to the user's location that's been obtained through the `fusedLocationProvider`. It also takes a `MapUiSettings` object, where you can tweak the visibility of elements like zoom buttons and my location button. Additionally, in `MapProperties` you can define attributes like the map type, style, and whether the user location is enabled.

Our complete fragment looks like this at the moment:

```kotlin
class LocationFragment : Fragment() {

    private val viewModel by viewModels<LocationViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                LocationScreen()
            }
        }
    }

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

        viewModel.fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity())
    }

    @OptIn(ExperimentalPermissionsApi::class, ExperimentalAnimationApi::class)
    @Composable
    fun LocationScreen(modifier: Modifier = Modifier) {
        val locationPermissionState = rememberMultiplePermissionsState(
            listOf(
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION
            )
        )

        LaunchedEffect(locationPermissionState.allPermissionsGranted) {
            if (locationPermissionState.allPermissionsGranted) {
                if (locationEnabled()) {
                    viewModel.getCurrentLocation()
                } else {
                    viewModel.locationState = LocationState.LocationDisabled
                }
            }
        }

        AnimatedContent(
            viewModel.locationState
        ) { state ->
            when (state) {
                is LocationState.NoPermission -> {
                    Column {
                        Text("We need location permission to continue")
                        Button(onClick = { locationPermissionState.launchMultiplePermissionRequest() }) {
                            Text("Request permission")
                        }
                    }
                }
                is LocationState.LocationDisabled -> {
                    Column {
                        Text("We need location to continue")
                        Button(onClick = { requestLocationEnable() }) {
                            Text("Enable location")
                        }
                    }
                }
                is LocationState.LocationLoading -> {
                    Text("Loading Map")
                }
                is LocationState.Error -> {
                    Column {
                        Text("Error fetching your location")
                        Button(onClick = { viewModel.getCurrentLocation() }) {
                            Text("Retry")
                        }
                    }
                }
                is LocationState.LocationAvailable -> {
                    val cameraPositionState = rememberCameraPositionState {
                        position = CameraPosition.fromLatLngZoom(state.location, 15f)
                    }
                    val mapUiSettings by remember { mutableStateOf(MapUiSettings()) }
                    val mapProperties by remember { mutableStateOf(MapProperties(isMyLocationEnabled = true)) }

                    GoogleMap(
                        modifier = Modifier.fillMaxSize(),
                        cameraPositionState = cameraPositionState,
                        uiSettings = mapUiSettings,
                        properties = mapProperties
                    )
                }
            }
        }
    }

    private fun locationEnabled(): Boolean {
        val locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager
        return LocationManagerCompat.isLocationEnabled(locationManager)
    }

    private fun requestLocationEnable() {
        activity?.let {
            val locationRequest = LocationRequest.create()
            val builder = LocationSettingsRequest
                .Builder()
                .addLocationRequest(locationRequest)
            val task = LocationServices
                .getSettingsClient(it)
                .checkLocationSettings(builder.build())
                .addOnSuccessListener {
                    if (it.locationSettingsStates?.isLocationPresent == true) {
                        viewModel.getCurrentLocation()
                    }
                }
                .addOnFailureListener {
                    if (it is ResolvableApiException) {
                        try {
                            it.startResolutionForResult(requireActivity(), 999)
                        } catch (e : IntentSender.SendIntentException) {
                            e.printStackTrace()
                        }
                    }
                }

        }
    }
}
```

And our app looks like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673182553752/82626295-7bb2-4c98-ad36-ecdf98bfaf4a.jpeg align="center")

### Let's add a marker

The `GoogleMap` composable has a content parameter, so we can pass it more composables that it'll render on the surface. One such Composable is `Marker`. Here's how you can use it:

```kotlin
GoogleMap(
    modifier = Modifier.fillMaxSize(),
    cameraPositionState = cameraPositionState,
    uiSettings = mapUiSettings,
    properties = mapProperties
) {
    Marker(
        state = rememberMarkerState(position = state.location)
    )
}
```

This code emits a marker that's always centered on the map since it uses the map's camera position to derive its own position. The user can move the map to center the marker anywhere they'd like. You can also add attributes like title, snippets and even configure onClick behavior.

## 3\. Places API

We use the Places API to provide autocomplete in textfields. Our intention is to achieve UI like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673192571631/4dd87c85-e5af-4010-b547-8448d5dc53b2.gif align="center")

Here, the user can search for any place and select a result from the autocomplete. Upon selection, the map moves to the place as well.

**Prerequisites:**

To use the places API, first ensure it's enabled in the [Google Cloud Console](https://console.cloud.google.com/apis/library/places-backend.googleapis.com).

Afterward, add this library to your app-level `build.gradle`:

```kotlin
dependencies {
    implementation 'com.google.android.libraries.places:places:3.0.0'
}
```

That's it, we're now ready to use the Places API.

It's time to add a TextField to our composable so that the user can search for places. We can add one to the bottom of the map using a `Box`. See the code below:

```kotlin
Box(
    modifier = Modifier.fillMaxSize()
) {
    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState,
        uiSettings = mapUiSettings,
        properties = mapProperties,
        onMapClick = {
            scope.launch {
                cameraPositionState.animate(CameraUpdateFactory.newLatLng(it))
            }
        }
    )
    
    Surface(
        modifier = Modifier
            .align(Alignment.BottomCenter)
            .padding(8.dp)
            .fillMaxWidth(),
        color = Color.White,
        shape = RoundedCornerShape(8.dp)
    ) {
        Column(
            modifier = Modifier
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            var text by remember { mutableStateOf("") }

            AnimatedVisibility(
                viewModel.locationAutofill.isNotEmpty(),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(8.dp)
            ) {
                LazyColumn(
                    verticalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    items(viewModel.locationAutofill) {
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(16.dp)
                                .clickWithRipple {
                                    text = it.address
                                    viewModel.locationAutofill.clear()
                                    viewModel.getCoordinates(it)
                                }
                        ) {
                            Text(it.address)
                        }
                    }
                }
                Spacer(Modifier.height(16.dp))
            }
            OutlinedTextField(
                value = text,
                onValueChange = {
                    text = it
                    viewModel.searchPlaces(it)
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(8.dp)
            )
        }
    }
}
```

We add a surface towards the bottom of the map, and use a column to position a textfield. There's a LazyColumn above that which appears when there are autofill results.

To handle places autocomplete results, we construct a data class. It contains a full address and a place ID, which we can use to obtain the lat-long for the place.

```kotlin
data class AutocompleteResult(
    val address: String,
    val placeId: String
)
```

Then, we use a `SnapshotStateList`, which is a state-aware list that'll store any autofill results that are made available via the search.

```kotlin
val locationAutofill = mutableStateListOf<AutocompleteResult>()
```

Now, whenever the user inputs a query, the `searchPlaces` method in the ViewModel is called:

```kotlin
private var job: Job? = null

fun searchPlaces(query: String) {
    job?.cancel()
    locationAutofill.clear()
    job = viewModelScope.launch {
        val request = FindAutocompletePredictionsRequest
            .builder()
            .setQuery(query)
            .build()
        placesClient
            .findAutocompletePredictions(request)
            .addOnSuccessListener { response ->
                locationAutofill += response.autocompletePredictions.map {
                    AutocompleteResult(
                        it.getFullText(null).toString(),
                        it.placeId
                    )
                }
            }
            .addOnFailureListener {
                it.printStackTrace()
                println(it.cause)
                println(it.message)
            }
    }
}
```

Notice the use of a `CoroutineJob` to handle debouncing. Since this method is called every time the user inputs a new character in the field, it can be called quite often, resulting in numerous expensive searches. To reduce the amount, we perform the whole operation inside a Coroutine, which is canceled whenever the method is called. This ensures that the method is called only for the last input character.

The API has several filters like country, location, and radius available which you can use to optimize your results.

When autocomplete results are available, the `SnapshotStateList` is updated with them, and the changes are immediately reflected in the UI. Now, to handle movement to the place on which the user has clicked, we define a new state object called `currentLatLang`.

```kotlin
var currentLatLong by mutableStateOf(LatLng(0.0, 0.0))
```

In our composable, we have a LaunchedEffect that's triggered everytime this state updates. It executes a camera movement:

```kotlin
LaunchedEffect(viewModel.currentLatLong) {
    cameraPositionState.animate(CameraUpdateFactory.newLatLng(viewModel.currentLatLong))
}
```

Now, to update this state object, we call the `getCoordinates` method. Remember, in our autocomplete results, we have a place ID, not coordinates. To get the lat-long for a place ID, we can use this code:

```kotlin
fun getCoordinates(result: AutocompleteResult) {
        val placeFields = listOf(Place.Field.LAT_LNG)
        val request = FetchPlaceRequest.newInstance(result.placeId, placeFields)
        placesClient.fetchPlace(request)
            .addOnSuccessListener {
                if (it != null) {
                    currentLatLong = it.place.latLng!!
                }
            }
            .addOnFailureListener {
                it.printStackTrace()
            }
    }
```

So, as soon as the user selects an autocomplete result in the UI, this method is called and executes an update in the map's camera position. We have successfully implemented the Places Autocomplete API and integrated it with our `GoogleMap`. yay!

## 4\. Reverse Geocoding API

Now we need to handle the last use-case where the address in the textfield should update whenever the marker is moved. To do this, we use the process of Reverse Geocoding, where we convert lat-long coordinates to an address.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673194945393/23a7760b-90b2-4afe-90f8-c7a8c39611b3.gif align="center")

Handling this case is sufficiently trivial. First, we move the `text` state variable that's used by the `TextField` to the ViewModel. Once that's done, let's add a `LaunchedEffect` that'll tell us when the camera position has changed:

```kotlin
LaunchedEffect(cameraPositionState.isMoving) {
    if (!cameraPositionState.isMoving) {
        viewModel.getAddress(cameraPositionState.position.target)
    }
}
```

This effect is triggered everytime the camera settles down after moving. In our ViewModel, we simply use the Geocoding API to produce an address:

```kotlin
fun getAddress(latLng: LatLng) {
        viewModelScope.launch {
            val address = geoCoder.getFromLocation(latLng.latitude, latLng.longitude, 1)
            text = address?.get(0)?.getAddressLine(0).toString()
        }
    }
```

Since `text` is the state variable for the `TextField`, changing it results in the UI being updated as well. And with that, we've achieved all our goals.

# End Result

<iframe src="https://player.vimeo.com/video/787340093?h=46b1d4028a&badge=0&autopause=0&player_id=0&app_id=58479" width="406" height="864"></iframe>

Otherwise, see [here](https://user-images.githubusercontent.com/22092047/211208551-d61c4eb2-0890-48c2-9524-42e09c444c0c.mp4)

# Final Words and Code

We have successfully built the screen as per the stated goal. There may be **rough edges and bugs** throughout the code, it's meant to be used as a sample of how to implement these features and integrate them with one another.

After a little bit of cleanup, the codes look like below:

%[https://gist.github.com/sanskar10100/eeff4187c12dec8510ad895ad72a2a6a] 

---

**I hope this article was helpful to you. Consider leaving a thumbs up if you liked it.**
