Simple Google Map App - Jetpack Compose
Tags: android, kotlin, beginners, jetpackcompose
Published: 2022-09-03 01:05:39

**Step-by-step guides to implement Google Map app using Jetpack Compose components for the Android Maps SDK**

This simple Google Map app is based on the simplified version of sample app from this [Google Map compose library](https://github.com/googlemaps/android-maps-compose). In addition, I added the following features into this sample app:  
- Location Permission Request= 
- Device Location Setting Request

![Simple Google Map App Written in Jetpack Compose](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1kzkox904vmox84gazs5.gif)

## Setup Google Cloud Project
The first thing you need to do is setting up a Google cloud project to generate an API key which allows you to use the Google Maps SDK API.

1. Setup New Project in [console.cloud.google.com](https://console.cloud.google.com/)
2. In your project dashboard, go to **APIs overview**
3. In **API & Services** page, go to **Library**
4. Search for **Maps SDK for Android** and enable it
5. Back to the **API & Services** page, go to **Credentials**
6. Select **+ CREATE CREDENTIALS**, then select **API key**
7. The API key is now generated. Click on the **API Key 1** to edit it. You can rename the API key name to whatever you like. For this sample app purpose, you do not need to set any restrictions on this API key. 
   - Select **None** for **Application restrictions**
   - Select **Don't restrict key** for **API restrictions**

These are just brief instructions. For detailed official instructions, see below:
- [Set Up in the Google Cloud Console](https://developers.google.com/maps/documentation/android-sdk/cloud-setup)
- [Using API keys](https://developers.google.com/maps/documentation/android-sdk/get-api-key)

> Please note I haven't setup any billing account or enable billing and it still works.

Once you have the API key, it is time to implement the code.

## 1. Add dependencies in `build.gradle`
These are the libraries needed to use Google Map compose library.
```
implementation 'com.google.maps.android:maps-compose:2.1.1'  
implementation 'com.google.android.gms:play-services-maps:18.0.2'  
implementation "androidx.compose.foundation:foundation:1.2.0-beta02"
```

## 2. Setup Secrets Gradle Plugin
[Secrets Gradle Plugin](https://github.com/google/secrets-gradle-plugin) is basically a library to help you hide your API key without committing it to the version control system.

It allows you to define your variable (e.g. API key) in the `local.properties` file (which is not checked into version control) and retrieve the variable. For example, you can retrieve the variable in the `AndroidManifest.xml` file.

These are the steps to add the Secrets Gradle plugin.

In project level `build.gradle`:
```groovy
buildscript {  
  ...
  dependencies {  
    classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"  
  }  
}
```
In app level `build.gradle`:
```groovy
plugins {  
  ... 	
  id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'  
}
```

## 3. Add MAPS_API_KEY in `local.properties`
In `local.properties` file,  copy the API key you get from **Setup Google Cloud Project** steps above and paste it here.
```
MAPS_API_KEY=Your API Key here
``` 

## 4. Add meta-data in `AndroidManifest.xml`

In order to read the `MAPS_API_KEY` variable that you defined in `local.properties`, you need to add the `<meta-data>` in the `AndroidManifext.xml`. 

Add this `<meta-data>` tag within the `<application>` tag.
```xml
<application  
  ...
  <meta-data  android:name="com.google.android.geo.API_KEY"  
  android:value="${MAPS_API_KEY}" />
  ...
</application>
```
If you do not **Setup Secrets Gradle plugin** above, you will get this error:
```
Attribute meta-data#com.google.android.geo.API_KEY@value at AndroidManifest.xml:14:13-44 requires a placeholder substitution but no value for <MAPS_API_KEY> is provided.
```

## 5. Add Internet and Location Permissions

Since the app needs to access internet and location permissions, we add these permissions in the `AndroidManifest.xml`.
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
    ...
</manifest>
```
 
 ## 6.  Implement `GoogleMap()` and `Marker()`
`GoogleMap()` and `Marker()` are the composable functions from the library that we can call to show the map and the markers on the map.

The map shows the current position if available, and it is defaulted to Sydney.

```kotlin
@Composable  
private fun MyGoogleMap(  
    currentLocation: Location,  
    cameraPositionState: CameraPositionState,  
    onGpsIconClick: () -> Unit) {  
  
    val mapUiSettings by remember {  
        mutableStateOf(  
            MapUiSettings(zoomControlsEnabled = false)  
        )  
    }  
  
    GoogleMap(  
        modifier = Modifier.fillMaxSize(),  
        cameraPositionState = cameraPositionState,  
        uiSettings = mapUiSettings,  
    ){  
        Marker(  
            state = MarkerState(
                position = LocationUtils.getPosition(currentLocation)),  
            title = "Current Position"  
       )  
    }  
  
    GpsIconButton(onIconClick = onGpsIconClick)  
    
    DebugOverlay(cameraPositionState)  
}
```
By default, the zoom control is on. To turn it off, you create a new `MapUiSettings` and pass that into the `GoogleMap()` as parameter. 

The map also have GPS icon. When you click on it, it moves the camera to the current location.  It also requests location permission and to enable device location setting if those requests have not been granted before.

`DebugOverlay` just an overlay screen to show the current camera status and position.


## 7. Request Location Permission

To check whether the location permission has already been granted, you use `ContextCompat.checkSelfPermission()` API.

```kotlin
fun isLocationPermissionGranted(context: Context) : Boolean {  
    return (ContextCompat.checkSelfPermission(  
        context, 
        Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)  
}
```

If the location permission is not granted,  you setup the callback whether the permission is granted or denied using `rememberLauncherForActivityResult()` with `ActivityResultContracts.RequestPermission()`.

To request the location permission using, you call the `ActivityResultLauncher.launch()`.

```kotlin
@Composable  
fun LocationPermissionsDialog(  
    onPermissionGranted: () -> Unit,  
    onPermissionDenied: () -> Unit,  
) {  
    val requestLocationPermissionLauncher = rememberLauncherForActivityResult(  
        ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->  
  
        if (isGranted) {  
            onPermissionGranted()  
        } else {  
            onPermissionDenied()  
        }  
    }  
  
    SideEffect {
        requestLocationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)  
    }  
}
```

> Note: Inititally I used the [permissions library](https://google.github.io/accompanist/permissions/) from accompanist. It worked only if the permission is granted,. It did not work well when the permission denied and I want to request the permission again. So I decided to use `rememberLauncherForActivityResult` instead.


## 8. Enable Location Setting
When the location permission has already granted, you want to make sure the location setting is turned on. If it is off, you want to request the user to turn it on.

Similar to **request location permission** above, you use  `rememberLauncherForActivityResult()` to register enable location setting request callback. 

```kotlin
val enableLocationSettingLauncher = rememberLauncherForActivityResult(  
    contract = ActivityResultContracts.StartIntentSenderForResult()  
) { activityResult ->  
  if (activityResult.resultCode == Activity.RESULT_OK)  
        onSuccess()  
  
    else {  
        onFailure()  
    }  
}
```

To check whether the location setting is turned on, you call 
`SettingsClient.checkLocationSettings()` API which returns the `Task<LocationSettingsResponse>` which allows you to set up the  failure and success callbacks.

If the callback is failed, it means the device location setting is off. In that case, you want to request user to enable it (if it is resolvable - exception is `ResolvableApiException`). To do that, you call `ActivityResultLauncher.launch()` API with the resolution `PendingIntent` that you get from the exception.

```kotlin
val locationRequest = LocationRequest.create().apply {  
  priority = Priority.PRIORITY_HIGH_ACCURACY  
}  
val locationRequestBuilder = LocationSettingsRequest.Builder()  
    .addLocationRequest(locationRequest)  
val locationSettingsResponseTask = LocationServices.getSettingsClient(context)  
    .checkLocationSettings(locationRequestBuilder.build())  
  
locationSettingsResponseTask.addOnSuccessListener {  
  onSuccess()  
}  
  
locationSettingsResponseTask.addOnFailureListener { exception ->  
  if (exception is ResolvableApiException){  
        try {  
            val intentSenderRequest =  
                IntentSenderRequest.Builder(exception.resolution).build()  
            enableLocationSettingLauncher.launch(intentSenderRequest)  
  
        } catch (sendEx: IntentSender.SendIntentException) {  
            sendEx.printStackTrace()  
        }  
    } else {  
        onFailure()  
    }  
}
```
> Refer to `LocationSettingDialog()` in the source code.

## 8. Get the last known location

Finally, you want to get the last known location, which is also a current location if the device location setting is turned on.

First, you set up the `LocationCallback()` to receive the `LocationResult` which has the last known location information. The callback is then removed to save power.

To request the location update, you call `FusedLocationProviderClient.requestLocationUpdates()` API by passing in the `LocationRequest`, `LocationCallback` and `Looper`.


```kotlin
@SuppressLint("MissingPermission")  
fun requestLocationResultCallback(  
    fusedLocationProviderClient: FusedLocationProviderClient,  
    locationResultCallback: (LocationResult) -> Unit  
) {  
 
    val locationCallback = object : LocationCallback() {  
        override fun onLocationResult(locationResult: LocationResult) {  
            super.onLocationResult(locationResult)  
  
            locationResultCallback(locationResult)  
            fusedLocationProviderClient.removeLocationUpdates(this)  
        }  
    }  
  
	val locationRequest = LocationRequest.create().apply {  
	  interval = 0  
	  fastestInterval = 0  
	  priority = Priority.PRIORITY_HIGH_ACCURACY  
	}
    Looper.myLooper()?.let { looper ->  
    fusedLocationProviderClient.requestLocationUpdates(  
        locationRequest,  
        locationCallback,  
        looper  
        )  
    }  
}
```

## Conclusion
The app requests the location permission and request to enable location setting during start up. It requests again when the user click in the GPS icon (if requests haven't been granted). It also moves the camera back to current position when the GPS icon is clicked.

For details and if you want to play around with the app, refer to the following source code.

## Source Code
GitHub Repository: [Demo_SimpleGoogleMap](https://github.com/vinchamp77/Demo_SimpleGoogleMap)

---

*Originally published at [https://vtsen.hashnode.dev](https://vtsen.hashnode.dev/simple-google-map-app-jetpack-compose)*.