Skip to content
All posts
July 20, 20264 min read

Google Maps in Jetpack Compose: The Complete Guide

Adding Google Maps to a Compose app is straightforward once you know the Maps Compose library — markers, camera control, custom styling, and the performance and lifecycle gotchas that bite people first.

Jetpack ComposeGoogle MapsAndroidMapsKotlin
Share:

Maps used to mean wrestling a

code
MapView
into a
code
Fragment
and managing its lifecycle by hand. The Maps Compose library makes it declarative, and once you see the shape it's genuinely pleasant. But there are a few gotchas around state, performance, and keys that catch everyone the first time, so this is the practical version of the guide.

The Basic Map

The core composable is

code
GoogleMap
, and the camera is hoisted state you control with
code
rememberCameraPositionState
. That hoisting is the key idea: the map doesn't own its camera, you do, which is what makes it easy to move the camera in response to app events.

kotlin
val camera = rememberCameraPositionState {
    position = CameraPosition.fromLatLngZoom(LatLng(13.75, 100.50), 12f)
}

GoogleMap(
    modifier = Modifier.fillMaxSize(),
    cameraPositionState = camera,
) {
    Marker(state = MarkerState(position = LatLng(13.75, 100.50)), title = "Bangkok")
}

Markers, polylines, and circles are child composables of

code
GoogleMap
, so you declare your map content the same way you declare any other UI — by describing what should be there for the current state.

Get the API Key Setup Right

The most common "why is my map blank" is the API key. The key goes in the manifest, but the key string itself should come from a gitignored properties file, never hardcoded, and the key must be restricted in the Google Cloud console to your app's package and signing certificate. An unrestricted key in a shipped APK is a billing liability — anyone can extract and use it. Restricting it to your app means a leaked key is useless to anyone else.

Driving the Camera From State

Because the camera is hoisted, animating it is just calling a suspend function. I tie camera moves to app events — a user taps a list item, the camera flies to that location.

kotlin
LaunchedEffect(selected) {
    selected?.let {
        camera.animate(CameraUpdateFactory.newLatLngZoom(it.latLng, 15f))
    }
}

The thing to watch is not fighting the user. If you animate the camera on every state change, you'll yank it away while they're panning. I only drive the camera in response to explicit user intent, not on every recomposition.

Performance With Many Markers

A handful of markers is fine. Hundreds will make the map stutter and the UI thread sweat. The fix is clustering — grouping nearby markers into a single cluster marker that splits as the user zooms in. The Maps Compose utilities include clustering support, and switching to it is the difference between a smooth map and a janky one for any app that plots real datasets. Don't wait until it's slow; if you know you'll have many points, reach for clustering from the start.

Lifecycle and Memory

The library handles most lifecycle wiring for you, which is a relief compared to the old

code
MapView
days. The remaining responsibility is your own state: don't hold large bitmap marker icons in a way that leaks, and load custom marker images efficiently rather than decoding full-resolution images for tiny pins. The map itself is heavy, so on screens where it's only sometimes shown, compose it conditionally rather than keeping it alive off-screen.

Custom Styling Sets the Tone

A default Google map looks like everyone else's. A custom map style — muted colors, hidden points of interest, a palette that matches your app — makes the screen feel like part of your product rather than an embedded widget. You define the style as a JSON resource and pass it via map properties. It's a small effort that disproportionately improves how polished the screen feels, and it's the kind of detail that separates an app that looks shipped from one that looks like a tutorial.

If there's one mindset that smooths the whole experience, it's remembering that the map is a piece of declarative UI like any other, not a special black box. The camera is hoisted state you control, the markers are composables you declare for the current data, and the same Compose habits that serve you elsewhere — keeping state in one place, reacting to it rather than imperatively poking the view — apply directly. People get into trouble when they treat the map as a foreign object and start fighting it imperatively. Lean into the declarative model, restrict your key, cluster your markers, and the map becomes one of the more pleasant screens to build rather than the dreaded one.

Key Takeaways

  • Use the Maps Compose library; the camera is hoisted state via
    code
    rememberCameraPositionState
    , and map content is declared as child composables.
  • Keep the Maps API key in a gitignored file and restrict it to your package and signing cert in the Cloud console.
  • Animate the camera only in response to explicit user intent, so you don't fight the user's panning.
  • Switch to marker clustering early if you'll plot many points; it's the difference between smooth and janky.
  • Compose the heavy map conditionally when it's only sometimes shown, and load marker icons efficiently.
  • Apply a custom JSON map style so the map feels like part of your app, not an embedded default.
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this