Simple Google Map App - Jetpack Compose
Vincent Tsen
Posted on September 3, 2022
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. In addition, I added the following features into this sample app:
- Location Permission Request=
- Device Location Setting Request
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.
- Setup New Project in console.cloud.google.com
- In your project dashboard, go to APIs overview
- In API & Services page, go to Library
- Search for Maps SDK for Android and enable it
- Back to the API & Services page, go to Credentials
- Select + CREATE CREDENTIALS, then select API key
- 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:
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 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
:
buildscript {
...
dependencies {
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
}
}
In app level build.gradle
:
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.
<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
.
<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.
@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.
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()
.
@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 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.
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.
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
.
@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
Originally published at https://vtsen.hashnode.dev.
Posted on September 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.