Integrating Google Maps in Jetpack Compose

Integrating Google Maps into your Android application can provide valuable location-based services and enhance user experience. Jetpack Compose, Android’s modern UI toolkit, offers a declarative and efficient way to incorporate maps. However, integrating traditional Android Views, like Google Maps, into Compose requires using the AndroidView composable.

Why Integrate Google Maps?

  • Location Services: Provides location-based information, directions, and points of interest.
  • Enhanced User Experience: Visual representation of location data, improving usability.
  • Rich Interactions: Allows users to interact with maps, such as zooming, panning, and placing markers.

Prerequisites

Before integrating Google Maps, make sure you have the following:

  • Google Maps API Key: Obtain a Google Maps API key from the Google Cloud Console.
  • Project Setup: Set up your Android project with Jetpack Compose and required dependencies.

Step 1: Add Dependencies

Ensure you have the necessary dependencies in your build.gradle file:

dependencies {
    implementation("com.google.android.gms:play-services-maps:18.2.0")
    implementation("com.google.maps.android:android-maps-utils:3.0.0") // Optional: For map utilities
    implementation("androidx.compose.ui:ui:1.6.1")
    implementation("androidx.compose.ui:ui-tooling-preview:1.6.1")
    implementation("androidx.compose.material:material:1.6.1")
}

Step 2: Add API Key to AndroidManifest.xml

Add the following meta-data tag within the application tag in your AndroidManifest.xml file:

<manifest ...>
    <application ...>
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="YOUR_API_KEY"/>
    </application>
</manifest>

Replace YOUR_API_KEY with the API key you obtained from the Google Cloud Console.

Step 3: Create a Composable for Google Maps

Use the AndroidView composable to integrate the MapView from the Google Maps Android API into your Compose UI.


import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import androidx.compose.runtime.remember
import androidx.compose.runtime.LaunchedEffect
import android.content.Context
import androidx.compose.ui.platform.LocalContext

@Composable
fun GoogleMapView(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val mapView = remember {
        MapView(context).apply {
            id = com.google.maps.android.R.id.map
        }
    }

    AndroidView(
        factory = { mapView },
        update = { map ->
            map.getMapAsync { googleMap ->
                val sydney = LatLng(-34.0, 151.0) // Example location: Sydney, Australia
                googleMap.addMarker(
                    MarkerOptions()
                        .position(sydney)
                        .title("Marker in Sydney")
                )
                googleMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
            }
        },
        modifier = modifier
    )

    // Lifecycle handling
    val lifecycle = rememberMapLifecycleObserver(mapView)
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, lifecycle) {
        lifecycleOwner.lifecycle.addObserver(lifecycle)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycle)
        }
    }
}

Step 4: Implement Map Lifecycle Handling

Google Maps requires proper lifecycle management. Use a lifecycle observer to manage the MapView‘s lifecycle within the Compose environment. rememberMapLifecycleObserver handles initializing and cleaning up the MapView


import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.compose.ui.platform.LocalLifecycleOwner
import com.google.android.gms.maps.MapView

@Composable
fun rememberMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
    remember(mapView) {
        LifecycleEventObserver { source, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> mapView.onCreate(source.lifecycle.currentState.bundle)
                Lifecycle.Event.ON_START -> mapView.onStart()
                Lifecycle.Event.ON_RESUME -> mapView.onResume()
                Lifecycle.Event.ON_PAUSE -> mapView.onPause()
                Lifecycle.Event.ON_STOP -> mapView.onStop()
                Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
                Lifecycle.Event.ON_ANY -> throw IllegalStateException()
            }
        }
    }

Step 5: Use GoogleMapView in Your Compose UI

Integrate the GoogleMapView composable into your main UI:


import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun MainScreen() {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colors.background
    ) {
        GoogleMapView()
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MaterialTheme {
        MainScreen()
    }
}

Customizing the Map

You can further customize the map by adding markers, polylines, and other features. Below are some common customizations:

Adding Markers

To add markers, use the MarkerOptions class:


import com.google.android.gms.maps.model.MarkerOptions
import com.google.android.gms.maps.model.LatLng

fun addMarker(googleMap: GoogleMap, position: LatLng, title: String) {
    googleMap.addMarker(
        MarkerOptions()
            .position(position)
            .title(title)
    )
}

Moving the Camera

To move the camera, use the CameraUpdateFactory class:


import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.LatLng

fun moveCamera(googleMap: GoogleMap, location: LatLng, zoom: Float) {
    googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(location, zoom))
}

Enabling My Location

Enable the “My Location” feature by requesting necessary permissions and setting the isMyLocationEnabled property:


import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.appcompat.app.AppCompatActivity

fun enableMyLocation(googleMap: GoogleMap, activity: AppCompatActivity) {
    if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)
        == PackageManager.PERMISSION_GRANTED) {
        googleMap.isMyLocationEnabled = true
    } else {
        ActivityCompat.requestPermissions(
            activity,
            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
            REQUEST_LOCATION_PERMISSION
        )
    }
}

Troubleshooting Common Issues

  • Map Not Displaying: Ensure the API key is correctly set up in AndroidManifest.xml. Also check your API key restrictions in Google Cloud Console.
  • Lifecycle Issues: Incorrect handling of MapView lifecycle can lead to crashes or unexpected behavior. Use LifecycleObserver as shown above.
  • Permissions: Ensure you have requested the necessary permissions (e.g., ACCESS_FINE_LOCATION) at runtime.

Conclusion

Integrating Google Maps in Jetpack Compose requires careful handling of Android Views using AndroidView and managing the map’s lifecycle. By following these steps, you can seamlessly incorporate Google Maps into your Compose UI, providing a rich and interactive map experience for your users. Properly customized and integrated maps can significantly enhance the functionality and usability of your Android application.