Vincent Tsen
Android Kotlin Weekly

Android Kotlin Weekly

Simple Google Map App - Jetpack Compose

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

Vincent Tsen's photo
Vincent Tsen
·Aug 13, 2022·

6 min read

Simple Google Map App - Jetpack Compose

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

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:

Simple_Google_Map_App_Jetpack_Compose_01.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
  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"

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

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this