DEV Community

Cover image for Simple Google Map App - Jetpack Compose
Vincent Tsen
Vincent Tsen

Posted on • Edited on • Originally published at vtsen.hashnode.dev

Simple Google Map App - Jetpack Compose

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

Simple Google Map App Written in Jetpack Compose

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
  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:

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"
Enter fullscreen mode Exit fullscreen mode

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"  
  }  
}
Enter fullscreen mode Exit fullscreen mode

In app level build.gradle:

plugins {  
  ...   
  id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'  
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

## 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)  
}
Enter fullscreen mode Exit fullscreen mode

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)  
}
Enter fullscreen mode Exit fullscreen mode

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)  
    }  
}
Enter fullscreen mode Exit fullscreen mode

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()  
    }  
}
Enter fullscreen mode Exit fullscreen mode

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()  
    }  
}
Enter fullscreen mode Exit fullscreen mode

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  
        )  
    }  
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)