Mapbox Maps Compose Extension

June 30, 2026 · View on GitHub

Overview

The Mapbox Maps Compose Extension is a public library to extend the Mapbox Map to work together with Jetpack Compose UI framework.

A full overview of classes and interfaces can be found in our API documentation.

Working examples of the Compose extension can be found in our compose test application.

Explore This Guide

Getting Started With Compose Extension

This README is intended for developers who are interested in contributing or building an app that uses the Mapbox Maps Compose Extension. Please visit DEVELOPING.md for general information and instructions on how to use the Mapbox Maps Extension System. To add the compose extension to your project, you configure its dependency in your build.gradle files.

// In the root build.gradle file
// The Mapbox access token needs to a scope set to DOWNLOADS:READ
allprojects {
    repositories {
        maven {
            url 'https://api.mapbox.com/downloads/v2/releases/maven'
            authentication {
                basic(BasicAuthentication)
            }
            credentials {
                username = "mapbox"
                password = "INSERT_MAPBOX_ACCESS_TOKEN_HERE"
            }
        }
    }
}

// In your build.gradle, add the compose extension with your other dependencies.
dependencies {
  implementation 'com.mapbox.extension:maps-compose:11.26.0-rc.1'

  // Pick your versions of Android Mapbox Map SDK
  // Note that Compose extension is compatible with Maps SDK v11.0+.
  implementation 'com.mapbox.maps:android:11.26.0-rc.1'
}

You should also become familiar with Google's documentation for Jetpack Compose.

To start using Compose, you need to first add some build configurations to your project. Add the following definition to your app’s build.gradle file:

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.2"
    }
}

Tutorials

Place a Mapbox map to your app

Here's a simple example on using Compose extension to insert a Mapbox map to your app:

...
import com.mapbox.maps.extension.compose.MapboxMap

public class SimpleMapActivity : ComponentActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MapboxMap(modifier = Modifier.fillMaxSize())
    }
  }
  ...
}

Setup the map style and set the initial camera position

You can set the initial map style with MapStyle composable function and the initial camera position by creating and remember MapViewportState.

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MapboxMap(
        Modifier.fillMaxSize(),
        mapViewportState = rememberMapViewportState {
          setCameraOptions {
            center(Point.fromLngLat(24.9384, 60.1699))
            zoom(9.0)
          }
        },
        style = {
          MapStyle(style = Style.SATELLITE_STREETS)
        }
      )
    }
  }

Use raw MapboxMap methods through MapEffect or DisposableMapEffect

Mapbox Compose Extension is built around the MapView in the base maps SDK. It's unlikely that we will be able to cover the full API surface in this wrapper, so we expose the reference to the raw MapView so that you can use all the API surface inside a MapEffect.

Please note that using raw MapView APIs in MapEffect might introduce internal state changes that interferes with the Compose states, and might result in unexpected behaviours, please use it with caution.

The following example showcases how to turn on debug features using MapEffect:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MapboxMap(modifier = Modifier.fillMaxSize()) {
        // Get reference to the raw MapView using MapEffect
        MapEffect(Unit) { mapView ->
          // Use mapView to access all the Mapbox Maps APIs including plugins etc.
          // For example, to enable debug mode:
          mapView.debugOptions = setOf(
              MapViewDebugOptions.TILE_BORDERS,
              MapViewDebugOptions.PARSE_STATUS,
              MapViewDebugOptions.TIMESTAMPS,
              MapViewDebugOptions.COLLISION,
              MapViewDebugOptions.STENCIL_CLIP,
              MapViewDebugOptions.DEPTH_BUFFER,
              MapViewDebugOptions.MODEL_BOUNDS,
              MapViewDebugOptions.TERRAIN_WIREFRAME,
          )
        }
      }
    }
  }

Use Camera Animation / Viewport APIs

The camera/viewport animation of the map within Compose is exposed through MapViewportState, and internally it's implemented with the ViewportPlugin of the base maps SDK. Currently we expose high level camera animation APIs such as setCamera, easeTo, flyTo and the FollowPuckViewportState and OverviewViewportState with the DefaultViewportTransition.

The following example showcases adding a button to do a flyTo animation to the target camera position:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      // Hold the hoisted MapViewPortState to manipulate the map camera.
      val mapViewportState = rememberMapViewportState {
        // Set the initial camera position
        setCameraOptions {
          center(Point.fromLngLat(0.0, 0.0))
          zoom(0.0)
          pitch(0.0)
        }
      }
      Box(modifier = Modifier.fillMaxSize()) {
        // Add a MapboxMap with the mapViewportState
        MapboxMap(
          modifier = Modifier.fillMaxSize(),
          mapViewportState = mapViewportState
        )
        // Add a button on top of the map
        Button(
          onClick = {
            mapViewportState.flyTo(
              cameraOptions = cameraOptions {
                center(Point.fromLngLat(13.403, 52.562))
                zoom(14.0)
                pitch(45.0)
              },
              MapAnimationOptions.mapAnimationOptions { duration(5000) }
            )
          }
        ) {
          Text(text = "Animate camera with FlyTo")
        }
      }
    }
  }

Add Annotations to the map

The full Annotation support is added with the initial compose extension release 0.1.0. PointAnnotation/CircleAnnotation/PolygonAnnotation/PolylineAnnotation can be added as composable functions within the content of the MapboxMap composable function.

AnnotationGroup composable functions are also introduced additionally to efficiently add a list of annotations to the map, and Point and Circle annotations within the same group can be configured to be clustered.

Add a single CircleAnnotation to the map

The following example showcases adding one circle annotation to the map:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MapboxMap(modifier = Modifier.fillMaxSize()) {
        // Add a single circle annotation at null island.
        CircleAnnotation(
          point = Point.fromLngLat(0.0, 0.0)
        ) {
          interactionsState.onClicked {
            Toast.makeText(
              this@CircleAnnotationActivity,
              "Clicked on single Circle Annotation: $it",
              Toast.LENGTH_SHORT
            ).show()
            true
          }
            .onLongClicked {
              Toast.makeText(
                this@CircleAnnotationActivity,
                "Long Clicked on single Circle Annotation: $it",
                Toast.LENGTH_SHORT
              ).show()
              true
            }
          circleRadius = 20.0
          circleColor = Color.Blue
        }
      }
    }
  }

Add multiple CircleAnnotations to the map in a group(more efficient for large number of annotations)

Adding multiple Annotations to the map using AnnotationGroup is more efficient, as they are backed by the same AnnotationManager and will be processed in batch and rendered in the same layer.

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MapboxMap(modifier = Modifier.fillMaxSize()) {
        // Add a number of circle annotations as CircleAnnotationGroup
        CircleAnnotationGroup(
          annotations = POINTS_TO_ADD.map {
            CircleAnnotationOptions()
              .withPoint(it)
              .withCircleRadius(10.0)
              .withCircleColor(Color.RED)
          }
        ) {
          interactionsState.onClicked {
            Toast.makeText(
              this@CircleAnnotationActivity,
              "Clicked on Circle Annotation Cluster's item: $it",
              Toast.LENGTH_SHORT
            ).show()
            true
          }
            .onLongClicked {
              Toast.makeText(
                this@CircleAnnotationActivity,
                "Long clicked on Circle Annotation Cluster's item: $it",
                Toast.LENGTH_SHORT
              ).show()
              true
            }
        }
      }
    }
  }

Add multiple PointAnnotations to the map as cluster(only supported for PointAnnotation and CircleAnnotation)

The following example showcases adding multiple PointAnnotations with clustering support:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MapboxMap(modifier = Modifier.fillMaxSize()) {
        PointAnnotationGroup(
          annotations = points.map {
            PointAnnotationOptions()
              .withPoint(it)
          },
          annotationConfig = AnnotationConfig(
            annotationSourceOptions = AnnotationSourceOptions(
              clusterOptions = ClusterOptions(
                textColorExpression = Expression.color(Color.YELLOW),
                textColor = Color.BLACK,
                textSize = 20.0,
                circleRadiusExpression = literal(25.0),
                colorLevels = listOf(
                  Pair(100, Color.RED),
                  Pair(50, Color.BLUE),
                  Pair(0, Color.GREEN)
                )
              )
            )
          ),
        ) {
          // Apply icon image to the whole annotation group.
          iconImage = IconImage(ICON_FIRE_STATION)

          interactionsState.onClicked {
            Toast.makeText(
              this@PointAnnotationClusterActivity,
              "Clicked on Point Annotation Cluster: $it",
              Toast.LENGTH_SHORT
            ).show()
            true
          }
            .onLongClicked {
              Toast.makeText(
                this@PointAnnotationClusterActivity,
                "Long clicked on Circle Annotation Cluster's item: $it",
                Toast.LENGTH_SHORT
              ).show()
              true
            }
            .onClusterClicked {
              Toast.makeText(
                this@PointAnnotationClusterActivity,
                "On cluster Click - ID: ${it.clusterId}, points:  ${it.pointCount}",
                Toast.LENGTH_SHORT
              ).show()
              true
            }
            .onClusterLongClicked {
              Toast.makeText(
                this@PointAnnotationClusterActivity,
                "On cluster Long Click - ID: ${it.clusterId}, points:  ${it.pointCount}",
                Toast.LENGTH_SHORT
              ).show()
              true
            }
        }
      }
    }
  }

Add ViewAnnotation to the map

With ViewAnnotation support for the compose extension, you are able to add a ViewAnnotation composable function to the content of MapboxMap and set its content using Android Composable functions.

The following example showcases adding ViewAnnotation that holds a Button to the map:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MapboxMap(modifier = Modifier.fillMaxSize()) {
        // Add a ViewAnnotation to the map
        ViewAnnotation(
          options = viewAnnotationOptions {
            // set the view annotation associated geometry
            geometry(Point.fromLngLat(0.0, 0.0))
            annotationAnchor {
              anchor(ViewAnnotationAnchor.BOTTOM)
            }
            allowOverlap(false)
          }
        ) {
          // You can add the content to be drawn in the ViewAnnotation using Composable functions, e.g. to insert a button:
          Button(
            onClick = {
              Toast.makeText(applicationContext, "Click", LENGTH_SHORT).show()
            }
          ) {
            Text("Click me")
          }
        }
      }
    }
  }

The Map ornaments are introduced as composable functions within dedicated scope and can be set to the MapboxMap composable function.

The following example showcases customising map ornaments:

MapboxMap(
  modifier = Modifier.fillMaxSize(),
  compass = {
    // Original compass
    Compass()
    // Add another compass with customised position and image
    Compass(
      modifier = Modifier.padding(scaffoldPadding),
      contentPadding = PaddingValues(0.dp),
      alignment = Alignment.TopEnd,
    ) {
      Image(
        painter = painterResource(id = R.drawable.my_compass),
        alpha = 0.9f,
        modifier = Modifier
          .height(55.dp)
          .width(55.dp),
        contentDescription = "My customised compass"
      )
    }
  },
  scaleBar = {
    // Change the ScaleBar primary color
    ScaleBar(
      modifier = Modifier.padding(scaffoldPadding),
      primaryColor = color
    )
  },
  attribution = {
    // Change the Attribution color
    Attribution(
      modifier = Modifier.padding(scaffoldPadding),
      iconColor = color
    )
  },
  logo = {
    // Change the Mapbox logo position
    Logo(modifier = Modifier.padding(scaffoldPadding), alignment = Alignment.BottomEnd)
  }
)

Gestures settings

The following example showcases how to change the GesturesSettings through hoisted MapState:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      // Hold the hoisted mapState to manipulate the map settings including gestures settings.
      val mapState = rememberMapState()
      Box(modifier = Modifier.fillMaxSize()) {
        // Add a MapboxMap with the mapState.
        MapboxMap(
          modifier = Modifier.fillMaxSize(),
          mapState = mapState
        )
        // Add a button on top of the map
        Button(
          onClick = {
            mapState.apply {
              gesturesSettings = gesturesSettings.toBuilder().setScrollEnabled(false).build()
            }
          }
        ) {
          Text(text = "Disable scroll gesture")
        }
      }
    }
  }

Work with runtime styling

The map style can be set through MapboxStyleComposable, currently we expose following Style composable functions:

  • GenericStyle for all the available style features as weakly typed APIs, it has full flexibility to control any style/slot/positioned layers and style import configs, but alternative strongly typed APIs should be preferred for safety and convenience to use.
  • MapStyle for simple loading style use cases if you don't need slots position your layer according to the layerIds defined in the style json.
  • MapboxStandardStyle for the default Mapbox Standard Style, it exposes available slots to position runtime-added layers and style import configs as strongly typed API.

Runtime styling with layers and sources

The layers can be added to the style/map as MapboxMapComposable functions.

  • When added to the slots or layerPositions within the MapboxStyleComposable, the layer will be added to the style after the style is loaded.
  • When added to the content of MapboxMap composable function, the layer will be added to the map immediately as persistent layer.

The sources are exposed as source state, which can be hoisted outside of the map and be shared with multiple layers. Please note the source state can not be shared across multiple map instances.

Please also note that the layer id and source id are automatically generated and remembered by default, so that you can reuse the layers in different places without layerId already exist error. In case if you need the layer/source id later for other purpose, e.g. query rendered features, you can provide your own id and reuse later.

The following example showcases how to work with runtime styling with composable functions:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      // Create a geoJsonSourceState to be used later with layers.
      val geoJsonSource: GeoJsonSourceState = rememberGeoJsonSourceState {
        // Set the initial geoJsonData as a Point
        data = GeoJSONData(Point.fromLngLat(0.0, 0.0))
      }
      MapboxMap(
        style = {
          // Load mapbox standard style
          MapboxStandardStyle(
            // Add a background layer to the 'top' slot of the standard style
            topSlot = {
              BackgroundLayer {
                backgroundColor = ColorValue(Color.Yellow)
                backgroundOpacity = DoubleValue(0.3)
              }
            }
          ) {
            // Set the light preset of Mapbox standard style to dawn through configuration.
            lightPreset = LightPresetValue.DAWN
          }
        }
      ) {
        // Insert a circle layer with the given geoJsonSource, to display a background circle.
        CircleLayer(
          sourceState = geoJsonSource
        ) {
          circleColor = ColorValue(Color.Cyan)
          circleRadius = DoubleValue(50.0)
          circleRadiusTransition = Transition(durationMillis = 1000L)
        }
        // Insert a symbol layer with the same geoJsonSource, to display a text above the circle.
        SymbolLayer(
          sourceState = geoJsonSource
        ) {
          textField = FormattedValue("Hello")
          textColor = ColorValue(Color.Black)
          // Use expression to set the text size as data driven property
          textSize = DoubleValue(
            Expression.interpolate {
              linear()
              zoom()
              stop {
                literal(0.0)
                literal(10.0)
              }
              stop {
                literal(10.0)
                literal(20.0)
              }
            }
          )
        }
      }
    }
  }

Compatibility with Maps SDK v11

The Compose extension is released separately from the Android Maps SDK v11 and has a compileOnly dependency. When using the Compose extension you need to include a compatible Maps SDK. The feature compatibility checklist can be found below.

Below is the full feature compatibility table:

FeaturesSupported?Compatible Maps SDK version
Basic Map renderingv11.0.0+
Annotations supportv11.0.0+
ViewAnnotation supportv11.0.0+
MapViewportState supportv11.0.0+
Gestures settings supportv11.0.0+
Access to raw MapView using MapEffectv11.0.0+
Map ornament(Compass, ScaleBar, Attribution, Logo) supportv11.3.0+
Style composable function supportv11.3.0+
Style layer/source supportv11.3.0+
Style projection supportv11.3.0+
Style terrain supportv11.5.0+
Style light supportv11.5.0+

View LICENSE.md for all dependencies used by this extension.