Rendering a map with many markers efficiently requires clustering (grouping nearby markers) and careful performance considerations. Showing 500 individual markers on a map without clustering is unusable.
Google Maps SDK Setup
// implementation("com.google.android.gms:play-services-maps:18.2.0")
// implementation("com.google.maps.android:maps-ktx:5.0.0") // Kotlin extensions
// implementation("com.google.maps.android:maps-utils-ktx:5.0.0") // Clustering, utils
// In Fragment
class MapFragment : Fragment() {
private lateinit var map: GoogleMap
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View {
return inflater.inflate(R.layout.fragment_map, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
viewLifecycleOwner.lifecycleScope.launch {
map = mapFragment.awaitMap() // maps-ktx suspend extension
setupMap()
}
}
private fun setupMap() {
map.apply {
uiSettings.isZoomControlsEnabled = true
uiSettings.isMyLocationButtonEnabled = false // use custom button
mapType = GoogleMap.MAP_TYPE_NORMAL
}
}
}
Marker Clustering
// Item for clustering
class RestaurantItem(val restaurant: Restaurant) : ClusterItem {
private val position = LatLng(restaurant.lat, restaurant.lng)
override fun getPosition() = position
override fun getTitle() = restaurant.name
override fun getSnippet() = restaurant.cuisine
override fun getZIndex() = 0f
}
// Setup clustering
class RestaurantMapFragment : Fragment() {
private lateinit var clusterManager: ClusterManager<RestaurantItem>
private fun setupClusterManager() {
clusterManager = ClusterManager<RestaurantItem>(requireContext(), map).apply {
setOnClusterClickListener { cluster ->
// Zoom to fit cluster
val builder = LatLngBounds.builder()
cluster.items.forEach { builder.include(it.position) }
map.animateCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 100))
true
}
setOnClusterItemClickListener { item ->
showRestaurantDetails(item.restaurant)
true
}
// Custom cluster renderer with icon
renderer = RestaurantClusterRenderer(requireContext(), map, this)
}
map.setOnCameraIdleListener(clusterManager)
map.setOnMarkerClickListener(clusterManager)
}
fun addRestaurants(restaurants: List<Restaurant>) {
clusterManager.clearItems()
clusterManager.addItems(restaurants.map { RestaurantItem(it) })
clusterManager.cluster()
}
}
Custom Cluster Renderer
class RestaurantClusterRenderer(
context: Context,
map: GoogleMap,
clusterManager: ClusterManager<RestaurantItem>
) : DefaultClusterRenderer<RestaurantItem>(context, map, clusterManager) {
override fun onBeforeClusterItemRendered(item: RestaurantItem, markerOptions: MarkerOptions) {
markerOptions
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE))
.title(item.restaurant.name)
}
override fun onBeforeClusterRendered(
cluster: Cluster<RestaurantItem>,
markerOptions: MarkerOptions
) {
// Draw a custom cluster icon showing the count
val clusterIcon = drawClusterIcon(cluster.size)
markerOptions.icon(BitmapDescriptorFactory.fromBitmap(clusterIcon))
}
private fun drawClusterIcon(count: Int): Bitmap {
val size = 96
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#FF5722")
}
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = 32f
textAlign = Paint.Align.CENTER
}
canvas.drawCircle(size / 2f, size / 2f, size / 2f, circlePaint)
canvas.drawText(count.toString(), size / 2f, size / 2f + 12f, textPaint)
return bitmap
}
}
Performance: Viewport-Only Markers
For very large datasets, only add markers visible in the current viewport:
map.setOnCameraIdleListener {
val bounds = map.projection.visibleRegion.latLngBounds
val visibleRestaurants = allRestaurants.filter { restaurant ->
bounds.contains(LatLng(restaurant.lat, restaurant.lng))
}
clusterManager.clearItems()
clusterManager.addItems(visibleRestaurants.map { RestaurantItem(it) })
clusterManager.cluster()
}
Heatmaps for Density
// implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
val heatmapData = restaurants.map { WeightedLatLng(LatLng(it.lat, it.lng), it.ratingWeight) }
val heatmapTileProvider = HeatmapTileProvider.Builder()
.weightedData(heatmapData)
.radius(50)
.maxIntensity(10.0)
.build()
map.addTileOverlay(TileOverlayOptions().tileProvider(heatmapTileProvider))
Key Takeaways
| Technique | When to use |
|---|---|
ClusterManager | 50+ markers on screen at any zoom level |
| Custom cluster renderer | Branded cluster icons; show count; accessibility |
| Viewport-only markers | 10,000+ items; only load what's visible |
CameraIdleListener | Reload/refilter markers after camera stops moving |
| Heatmaps | Density visualization (e.g., ride demand, restaurant density) |
awaitMap() | Kotlin coroutine extension; cleaner than getMapAsync |