Integrating Google Maps into your Jetpack Compose application can significantly enhance the user experience by providing interactive and dynamic geographical information.
In this blog post, we'll show you how to add a map to your app, control the camera position, and draw various elements on the map. From setting targets and animating zoom levels to handling boundaries and drawing markers, circles, and polylines, you'll learn how to leverage the powerful features of Google Maps in Jetpack Compose.
If you're looking for a practical implementation of the techniques we will discuss in this blog, check out GroupTrack open-source location sharing and tracking application. Feel free to explore and use it as a reference for your projects.
Let's dive in and start integrating Google Maps into your app!
Add the following dependency to your module gradle.build
file,
implementation 'com.google.maps.android:maps-compose:X.X.X'
Add your Map API key in the manifest file,
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${YOUR_MAPS_API_KEY}" />
To render a map you need to use Google Composable. Let's have a quick look at it.
@Composable
public fun GoogleMap(
mergeDescendants: Boolean = false,
modifier: Modifier = Modifier,
cameraPositionState: CameraPositionState = rememberCameraPositionState(),
contentDescription: String? = null,
googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() },
properties: MapProperties = DefaultMapProperties,
locationSource: LocationSource? = null,
uiSettings: MapUiSettings = DefaultMapUiSettings,
indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener,
onMapClick: ((LatLng) -> Unit)? = null,
onMapLongClick: ((LatLng) -> Unit)? = null,
onMapLoaded: (() -> Unit)? = null,
onMyLocationButtonClick: (() -> Boolean)? = null,
onMyLocationClick: ((Location) -> Unit)? = null,
onPOIClick: ((PointOfInterest) -> Unit)? = null,
contentPadding: PaddingValues = NoPadding,
content: (@Composable @GoogleMapComposable () -> Unit)? = null,
)
Basic Setup:
modifier
: Apply visual styling to your map container.cameraPositionState
: Manage the map's viewpoint (zoom level and location).Customization Options:
googleMapOptionsFactory
: Fine-tune the initial map behavior (e.g., zoom controls, map type).properties
: Set additional map attributes like compass position and zoom gestures.locationSource
: Integrate location tracking features (if needed).uiSettings
: Control UI elements like zoom buttons or the compass.Event Listeners:
onMapClick
: Respond to user clicks on the map (e.g., display location info).onMapLongClick
(New!): Handle long presses on the map (coming soon!).onMapLoaded
: Execute code when the map finishes loading.onMyLocationButtonClick
: Handle clicks on the "My Location" button.onMyLocationClick
: Respond to user clicks on their current location marker.onPOIClick
: Capture user interactions with points of interest (POIs) on the map.Adding Content:
contentPadding
: Define padding around the map edges to avoid overlapping with other UI elements.content
: This is where you add your visual elements, like markers or overlays, on top of the map.Let's first render the Map, and then talk about map properties, type and UI settings.
@Composable
fun SimpleMap() {
val latLng = LatLng(-33.852334, 151.211245) // Any Coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f) // Set zoom level
}
GoogleMap(
modifier = Modifier.fillMaxSize(), // Fill the entire screen
cameraPositionState = cameraPositionState
)
}
Run your app and voila! You should see a centered map of Sydney.
Now, let’s delve deeper into the MapProperties
class, which allows you to refine your map's visual and interactive aspects.
public class MapProperties(
public val isBuildingEnabled: Boolean = false,
public val isIndoorEnabled: Boolean = false,
public val isMyLocationEnabled: Boolean = false,
public val isTrafficEnabled: Boolean = false,
public val latLngBoundsForCameraTarget: LatLngBounds? = null,
public val mapStyleOptions: MapStyleOptions? = null,
public val mapType: MapType = MapType.NORMAL,
public val maxZoomPreference: Float = 21.0f,
public val minZoomPreference: Float = 3.0f,
)
Building and Indoor Maps:
isBuildingEnabled
: Show or hide 3D building overlays (default: false).isIndoorEnabled
: Enable viewing indoor maps of supported locations (default: false).Location and Traffic:
isMyLocationEnabled
: Allow users to see their current location on the map (default: false).isTrafficEnabled
: Overlay real-time traffic information on the map (default: false).Map Appearance and Behavior:
latLngBoundsForCameraTarget
: Define a specific area for the map to focus on initially.mapStyleOptions
: Apply custom map styles to change the visual appearance (e.g., map theme).mapType
: Choose between different map types like normal, satellite, or hybrid (default: NORMAL).maxZoomPreference
: Set the maximum zoom level users can reach (default: 21.0f).minZoomPreference
: Set the minimum zoom level users can reach (default: 3.0f).Let’s render our previous map along with map properties:
@Composable
fun SimpleMap() {
val latLng = LatLng(-33.852334, 151.211245) // Any Coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f) // Set zoom level
}
val mapProperties = remember(isDarkMode) {
MapProperties(
latLngBoundsForCameraTarget = LatLngBounds(
latLng,
LatLng(-33.852334 + 0.1, 151.211245 + 0.1)
),
mapType = MapType.HYBRID,
)
}
GoogleMap(
modifier = Modifier.fillMaxSize(), // Fill the entire screen
cameraPositionState = cameraPositionState,
properties = mapProperties,
)
}
So, after applying the above mapProperties
, your app will look like this:
Now, let's delve into MapUiSettings, which empowers you to fine-tune the user interaction experience with the map.
Key Settings in MapUiSettings
:
Basic Controls:
compassEnabled
: Show or hide the compass for directional reference (default: true).mapToolbarEnabled
: Enable or disable the default map toolbar with zoom and other controls (default: true).myLocationButtonEnabled
: Control whether the "My Location" button is displayed (default: true). Gesture Controls:
rotationGesturesEnabled
: Allow users to rotate the map by swiping (default: true).scrollGesturesEnabled
: Enable panning the map by dragging (default: true).scrollGesturesEnabledDuringRotateOrZoom
: Allow scrolling while rotating or zooming (default: true).tiltGesturesEnabled
: Enable tilting the map view (default: true).zoomControlsEnabled
: Show or hide the zoom buttons for easy zooming (default: true).zoomGesturesEnabled
: Allow users to zoom in and out using pinch gestures (default: true).Let’s integrate map ui settings in our SimpleMap composable:
@Composable
fun SimpleMap() {
val latLng = LatLng(-33.852334, 151.211245) // Any Coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f) // Set zoom level
}
val mapProperties = remember(isDarkMode) {
MapProperties(
latLngBoundsForCameraTarget = LatLngBounds(
latLng,
LatLng(-33.852334 + 0.1, 151.211245 + 0.1)
),
mapType = MapType.HYBRID,
)
}
GoogleMap(
modifier = Modifier.fillMaxSize(), // Fill the entire screen
cameraPositionState = cameraPositionState,
properties = mapProperties,
uiSettings = MapUiSettings(
myLocationButtonEnabled = false,
compassEnabled = false,
mapToolbarEnabled = false
),
)
}
Controlling the camera position allows you to direct the user’s focus on specific areas of the map and animate movements for a better experience.
Let’s break down the core features and methods for handling camera position:
Core Functionality:
Key Methods:
State Management:
GoogleMap
composable at a time.You can modify the camera position dynamically to zoom into a new location, set boundaries, or add custom animations.
Here are the key properties you can use to manipulate the camera:
Here’s how you can use these properties to change or animate the camera position:
@Composable
fun SimpleMap() {
val latLng = LatLng(-33.852334, 151.211245) // Any Coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f) // Set zoom level
}
val mapProperties =
remember(isDarkMode) {
MapProperties(
mapType = MapType.HYBRID,
)
}
GoogleMap(
modifier = Modifier.fillMaxSize(), // Fill the entire screen
cameraPositionState = cameraPositionState,
properties = mapProperties,
uiSettings =
MapUiSettings(
myLocationButtonEnabled = false,
compassEnabled = false,
mapToolbarEnabled = false
),
onMapClick = { clickedLatLng: LatLng? ->
// Animate camera to the clicked location
coroutineScope.launch {
clickedLatLng?.let {
cameraPositionState.animate(
CameraUpdateFactory.newLatLng(clickedLatLng),
1000 // Animation duration in milliseconds
)
}
}
}
)
// Button to move camera to a predefined position (e.g., Melbourne)
Button(
onClick = {
val newLatLng = LatLng(-37.8136, 144.9631) // Melbourne coordinates
coroutineScope.launch {
cameraPositionState.animate(
CameraUpdateFactory.newLatLngZoom(newLatLng, 12f),
2000 // Animate over 2 seconds
)
}
},
modifier = Modifier.align(Alignment.TopEnd).padding(16.dp)
) {
Text(
"Move to Melbourne",
style = AppTheme.appTypography.button.copy(color = AppTheme.colorScheme.onPrimary)
)
}
}
You can also define zoom limits, boundaries for camera movement, and other settings to restrict how users interact with the map.
CameraUpdateFactory.newLatLngZoom()
.LatLngBounds
.@Composable
fun SimpleMap(
) {
val latLng = LatLng(-33.852334, 151.211245) // Initial Coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f)
}
val bounds = LatLngBounds(
LatLng(-33.942334, 151.111245), // South-West corner
LatLng(-33.762334, 151.311245) // North-East corner
)
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
properties = MapProperties(
latLngBoundsForCameraTarget = bounds, // Apply boundaries
minZoomPreference = 10f, // Minimum zoom level
maxZoomPreference = 18f // Maximum zoom level
)
)
}
As per this setup:
LatLngBounds
).
After controlling the camera, you may want to draw elements like markers, circles, or polylines on the map to represent points of interest, routes, or other data.
Markers are icons that pin-point specific locations on the map.
@Composable
fun SimpleMap(
) {
val latLng = LatLng( -33.8568, 151.2153) // Sydney Opera House lat lng
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f)
}
val mapProperties = remember {
MapProperties(
mapType = MapType.HYBRID,
)
}
GoogleMap(
cameraPositionState = cameraPositionState,
properties = mapProperties,
uiSettings = MapUiSettings(
myLocationButtonEnabled = false,
compassEnabled = false,
mapToolbarEnabled = false
),
) {
Marker(
state = rememberMarkerState(position = latLng),
title = "Sydney Opera House",
snippet = "The most famous opera house in the world",
)
}
}
In the above example:
To customize the markers, we’ll use the MarkerComposable
instead of the standard Marker
composable. The MarkerComposable
allows you to render custom content as a marker by using Compose UI components, offering greater flexibility in design. In contrast, the standard Marker
composable only lets you customize the marker icon but does not support more intricate layouts or styles.
@Composable
fun SimpleMap(
) {
val latLng = LatLng( -33.8568, 151.2153) // Sydney Opera House lat lng
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f)
}
val mapProperties = remember {
MapProperties(
mapType = MapType.HYBRID,
)
}
GoogleMap(
cameraPositionState = cameraPositionState,
properties = mapProperties,
uiSettings = MapUiSettings(
myLocationButtonEnabled = false,
compassEnabled = false,
mapToolbarEnabled = false
),
) {
MarkerComposable(
state = rememberMarkerState(position = latLng),
title = "Sydney Opera House"
) {
Box(
modifier = Modifier
.size(80.dp)
.background(
Color.Red.copy(alpha = 0.4f),
shape = CircleShape
)
.border(0.5.dp, Color.Red.copy(alpha = 0.8f), shape = CircleShape),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(40.dp)
.background(
AppTheme.colorScheme.surface,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.drawable.ic_tab_places_filled),
contentDescription = null,
tint = AppTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
}
}
}
}
}
A Circle allows you to highlight a circular area on the map, usually centered around a specific location. You can customize the radius, stroke, and fill colors.
@Composable
fun SimpleMap(
) {
val latLng = LatLng(-33.8568, 151.2153) // Sydney Opera House lat lng
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f)
}
val mapProperties = remember {
MapProperties(
mapType = MapType.HYBRID,
)
}
GoogleMap(
cameraPositionState = cameraPositionState,
properties = mapProperties,
uiSettings = MapUiSettings(
myLocationButtonEnabled = false,
compassEnabled = false,
mapToolbarEnabled = false
),
) {
Circle(
center = latLng,
radius = 1000.0,
strokeColor = Color.Red,
fillColor = Color(0x220000FF),
strokeWidth = 2f
)
}
}
To further enhance the map, you can create a custom circle that adjusts its size based on the zoom level. This is done by converting the radius from meters to pixels and drawing the circle with a Canvas
.
@Composable
fun CustomCircleMap() {
val latLng = LatLng(-33.8568, 151.2153) // Sydney Opera House lat lng
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 15f)
}
val mapProperties = remember {
MapProperties(
mapType = MapType.HYBRID,
)
}
// Define the initial radius in meters
val radiusMeters by remember {
mutableDoubleStateOf(200.0)
}
// Compute the circle's radius in pixels based on the current zoom level
val projection = cameraPositionState.projection
val points = projection?.toScreenLocation(cameraPositionState.position.target) ?: Point(0, 0)
val radiusInPixels = convertZoneRadiusToPixels(
cameraPositionState.position.target,
radiusMeters,
projection
)
Box(modifier = Modifier.fillMaxSize()) {
GoogleMap(
cameraPositionState = cameraPositionState,
properties = mapProperties,
uiSettings = MapUiSettings(
myLocationButtonEnabled = false,
compassEnabled = false,
mapToolbarEnabled = false
),
)
Box(
modifier = Modifier
.clipToBounds(),
contentAlignment = Alignment.Center
) {
val color = AppTheme.colorScheme.primary.copy(alpha = 0.6f)
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(
color = color,
radius = radiusInPixels.toFloat(),
center = Offset(points.x.toFloat(), points.y.toFloat())
)
}
Box(
modifier = Modifier
.size(40.dp)
.align(Alignment.Center)
.background(AppTheme.colorScheme.surface, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.drawable.ic_tab_places_filled),
contentDescription = "",
modifier = Modifier.size(20.dp),
tint = AppTheme.colorScheme.primary
)
}
}
}
}
// Converts the radius from meters to pixels based on the current zoom level
private fun convertZoneRadiusToPixels(
latLng: LatLng,
radiusInMeters: Double,
projection: Projection?
): Int {
if (projection == null) return 0
// Radius of the Earth in meters
val EARTH_RADIUS = 6378100.0
val lat1 = radiusInMeters / EARTH_RADIUS
val lng1 = radiusInMeters / (EARTH_RADIUS * cos((Math.PI * latLng.latitude / 180)))
// Compute the bounding latLng
val lat2 = latLng.latitude + lat1 * 180 / Math.PI
val lng2 = latLng.longitude + lng1 * 180 / Math.PI
val p1: Point = projection.toScreenLocation(LatLng(latLng.latitude, latLng.longitude))
val p2: Point = projection.toScreenLocation(LatLng(lat2, lng2))
return abs((p1.x - p2.x).toDouble()).toInt()
}
In this example:
Canvas
, which allows for dynamic adjustment based on zoom level.convertZoneRadiusToPixels
function translates the radius from meters to pixels to ensure the circle’s size adjusts with the zoom level.This provides both a standard and a more flexible, zoom-responsive way to highlight areas on the map.
A Polyline represents a series of connected lines between different points on the map, often used to display routes or boundaries.
@Composable
fun SimpleMap(
) {
val latLng = LatLng(-33.8568, 151.2153) // Sydney Opera House lat lng
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f)
}
val mapProperties = remember {
MapProperties(
mapType = MapType.NORMAL,
)
}
val points = listOf(
LatLng(-33.852334, 151.211245), // Sydney Opera House
LatLng(-33.873651, 151.2068896), // Sydney Tower Eye
LatLng(-33.891475, 151.276684) // Bondi Beach
)
GoogleMap(
cameraPositionState = cameraPositionState,
properties = mapProperties,
uiSettings = MapUiSettings(
myLocationButtonEnabled = false,
compassEnabled = false,
mapToolbarEnabled = false
),
) {
Polyline(
points = points,
color = Color.Blue,
width = 7f
)
CustomMarkers(
latLng = points[0],
title = "Sydney Opera House"
)
CustomMarkers(
latLng = points[1],
title = "Sydney Tower Eye"
)
CustomMarkers(
latLng = points[2],
title = "Bondi Beach"
)
}
}
@Composable
fun CustomMarkers(latLng: LatLng, title: String) {
MarkerComposable(
state = rememberMarkerState(position = latLng),
title = title
) {
Box(
modifier = Modifier
.size(50.dp)
.background(
Color.Red.copy(alpha = 0.4f),
shape = CircleShape
)
.border(0.5.dp, Color.Red.copy(alpha = 0.8f), shape = CircleShape),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(30.dp)
.background(
AppTheme.colorScheme.surface,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.drawable.ic_tab_places_filled),
contentDescription = null,
tint = AppTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
}
}
}
}
Following this guide, you’ve successfully integrated Google Maps into your Jetpack Compose app, controlled the camera view, and added various map elements.
These features can significantly improve your app’s usability and appeal. Keep exploring and experimenting with Google Maps to unlock even more potential.
Check out GroupTrack — a location-sharing and tracking application. It covers all the features mentioned, including adding maps, controlling camera positions, and drawing markers, circles, and polylines.
Thanks for reading, and happy developing!