On this page

    Create and style clusters - Kotlin SDK

    The following Jetpack Compose example demonstrates how to use the built-in Kotlin SDK functions to generate and visualize a cluster map of points.

    Add an MTGeoJSONSource to the map’s style with isCluster: true. Adjust the clusterRadius and clusterMaxZoom parameters as needed.

    To render clusters, create an MTCircleLayer for the cluster bubbles. Use step expressions based on point count to determine color and size, and apply a cluster filter. Add an MTSymbolLayer above to display the cluster count using MTTextToken.pointCountAbbreviated. Finally, create another MTCircleLayer for individual (unclustered) points, and apply an unclustered filter to show only those.

    
    import android.graphics.Color
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.DisposableEffect
    import androidx.compose.runtime.LaunchedEffect
    import androidx.compose.runtime.remember
    import androidx.compose.ui.Modifier
    import com.maptiler.maptilersdk.MTConfig
    import com.maptiler.maptilersdk.events.MTEvent
    import com.maptiler.maptilersdk.map.MTMapOptions
    import com.maptiler.maptilersdk.map.MTMapView
    import com.maptiler.maptilersdk.map.MTMapViewController
    import com.maptiler.maptilersdk.map.MTMapViewDelegate
    import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle
    import com.maptiler.maptilersdk.map.style.MTMapStyleVariant
    import com.maptiler.maptilersdk.map.style.MTStyle
    import com.maptiler.maptilersdk.map.style.dsl.MTExpression
    import com.maptiler.maptilersdk.map.style.dsl.MTFilter
    import com.maptiler.maptilersdk.map.style.dsl.MTFeatureKey
    import com.maptiler.maptilersdk.map.style.dsl.PropertyValue
    import com.maptiler.maptilersdk.map.style.layer.circle.MTCircleLayer
    import com.maptiler.maptilersdk.map.style.layer.circle.colorConst
    import com.maptiler.maptilersdk.map.style.layer.circle.colorExpr
    import com.maptiler.maptilersdk.map.style.layer.circle.radiusConst
    import com.maptiler.maptilersdk.map.style.layer.circle.radiusExpr
    import com.maptiler.maptilersdk.map.style.layer.symbol.MTSymbolLayer
    import com.maptiler.maptilersdk.map.style.layer.symbol.textAllowOverlap
    import com.maptiler.maptilersdk.map.style.layer.symbol.MTTextAnchor
    import com.maptiler.maptilersdk.map.style.layer.symbol.textAnchor
    import com.maptiler.maptilersdk.map.style.layer.symbol.textColorConst
    import com.maptiler.maptilersdk.map.style.layer.symbol.textField
    import com.maptiler.maptilersdk.map.style.layer.symbol.textFont
    import com.maptiler.maptilersdk.map.style.layer.symbol.textSize
    import com.maptiler.maptilersdk.map.style.dsl.MTTextToken
    import com.maptiler.maptilersdk.map.style.source.MTGeoJSONSource
    import com.maptiler.maptilersdk.map.types.MTData
    import java.net.URL
    
    @Composable
    fun ClusteringCompose() {
      val controller = remember { MTMapViewController(baseContext) }
    
      LaunchedEffect(controller) {
        controller.delegate = object : MTMapViewDelegate {
          override fun onMapViewInitialized() {
            setupClusters(controller.style!!)
          }
    
          override fun onEventTriggered(event: MTEvent, data: MTData?) {
            // no-op
          }
        }
      }
    
      DisposableEffect(controller) { onDispose { controller.delegate = null } }
    
      MTMapView(
        referenceStyle = MTMapReferenceStyle.DATAVIZ,
        options = MTMapOptions(),
        controller = controller,
        modifier = Modifier.fillMaxSize(),
        styleVariant = MTMapStyleVariant.DARK,
      )
    }
    
    private fun setupClusters(style: MTStyle) {
      // 1) Source with clustering
      val src = MTGeoJSONSource.fromUrl(
        identifier = "earthquakes",
        url = URL("https://docs.maptiler.com/sdk-js/assets/earthquakes.geojson"),
      ).apply {
        isCluster = true
        clusterRadius = 50.0
        clusterMaxZoom = 14.0
      }
      style.addSource(src)
    
      // 2) Cluster circles (inline config)
      val clusters =
        MTCircleLayer(identifier = "clusters", sourceIdentifier = src.identifier)
          .apply {
            withFilter(MTFilter.clusters())
            colorExpr(
              MTExpression.step(
                input = MTExpression.get(MTFeatureKey.POINT_COUNT),
                default = PropertyValue.Color(Color.parseColor("#51bbd6")),
                stops = listOf(
                  100.0 to PropertyValue.Color(Color.parseColor("#f1f075")),
                  750.0 to PropertyValue.Color(Color.parseColor("#f28cb1")),
                ),
              ),
            )
            radiusExpr(
              MTExpression.step(
                input = MTExpression.get(MTFeatureKey.POINT_COUNT),
                default = PropertyValue.Num(20.0),
                stops = listOf(
                  100.0 to PropertyValue.Num(30.0),
                  750.0 to PropertyValue.Num(40.0),
                ),
              ),
            )
          }
      style.addLayer(clusters)
    
      // 3) Cluster count labels (inline config)
      val labels =
        MTSymbolLayer(identifier = "clusterCount", sourceIdentifier = src.identifier)
          .apply {
            withFilter(MTFilter.clusters())
            textField(MTTextToken.POINT_COUNT_ABBREVIATED)
            textSize(12.0)
            textAllowOverlap(true)
            textAnchor(MTTextAnchor.CENTER)
            textFont(listOf("DIN Offc Pro Medium", "Arial Unicode MS Bold"))
            textColorConst(Color.WHITE)
          }
      style.addLayer(labels)
    
      // 4) Unclustered points (inline config)
      val unclustered =
        MTCircleLayer(identifier = "unclusteredPoint", sourceIdentifier = src.identifier)
          .apply {
            withFilter(MTFilter.unclustered())
            colorConst(Color.parseColor("#11b4da"))
            radiusConst(4.0)
          }
      style.addLayer(unclustered)
    }
    
    /** Optional Activity wrapper to run the composable. */
    class ClusteringComposeActivity : ComponentActivity() {
      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MTConfig.apiKey = "YOUR_API_KEY"
        setContent { ClusteringCompose() }
      }
    }
    

    XML in Kotlin

    If you are working with XML in Kotlin refer to the Kotlin XML clustering example

    Was this helpful?

    Mobile SDK
    Android
    Create and style clusters - Kotlin SDK
    Clusters