Skip to content

Map objects

States API

The content of YandexMap() is a composition of its own, with map objects as nodes. Its composables are marked @YandexMapComposable, and Compose UI composables cannot be placed there.

Composable Draws
Placemark a point with an icon
TitledPlacemark a placemark with a text label
Polyline a line
Polygon a filled area
Circle a circle with a radius in metres
Clustering placemarks grouped into clusters
MapObjectCollection a nested group
MapObjectLayer a group on a separate layer
TileLayer, TrafficLayer map-wide layers
MapListeners map events

Every object composable takes a state object created with rememberXxxState(). Geometry lives on the state, not in a parameter, so moving an object is a state write and does not recompose the subtree.

Placemark

val placemarkGeometry = Point(55.751225, 37.629540)

@Composable
fun MapScreen() {
    val cameraPositionState = rememberCameraPositionState { position = startPosition }
    YandexMap(
        cameraPositionState = cameraPositionState,
        modifier = Modifier.fillMaxSize(),
    ) {
        Placemark(
            state = rememberPlacemarkState(placemarkGeometry),
            icon = imageProvider(Res.drawable.pin_red),
            iconStyle = IconStyle(anchor = PointF(0.5f, 1f), scale = 1.2f),
            onTap = { point -> true },
            visible = true,
            draggable = false,
            zIndex = 1f,
            opacity = 1f,
            userData = poi,
        )
    }
}

imageProvider(Res.drawable.pin_red) builds the icon from a Compose Multiplatform resource — see Image resources.

TitledPlacemark adds a label:

TitledPlacemark(
    state = rememberPlacemarkState(placemarkGeometry),
    icon = imageProvider(Res.drawable.pin_red),
    title = "Red Square",
    titleStyle = TextStyle(size = 10f, placement = TextStyle.Placement.BOTTOM, offset = 4f),
)

Circle

Circle(
    state = rememberCircleState(Circle(center = point, radius = 500f)),
    color = Color.Red.copy(alpha = 0.2f),
    strokeColor = Color.Red,
    strokeWidth = 2f,
    geodesic = false,
    onTap = { true },
)

Polyline

Polyline(
    state = rememberPolylineState(Polyline(points)),
    strokeColor = Color.Green,
    strokeWidth = 5f,
    outlineColor = Color.DarkGray,
    outlineWidth = 1f,
    dashLength = 8f,
    gapLength = 4f,
    turnRadius = 10f,
)

Polygon

Polygon(
    state = rememberPolygonState(
        Polygon(outerRing = LinearRing(outerPoints), innerRings = listOf(LinearRing(hole))),
    ),
    color = Color.Blue.copy(alpha = 0.2f),
    strokeColor = Color.Blue,
    strokeWidth = 2f,
    geodesic = true,
    pattern = imageProvider(Res.drawable.hatch),
    patternScale = 1f,
)

What the state adds

The state object is where everything a parameter cannot express lives. It is bound to the map object while the composable is in the composition, so these calls belong in an effect or an event handler, not in the composition itself.

Any object

mapObjectState.setVisible(
    visible = false,
    animation = Animation(Animation.Type.SMOOTH, 300.milliseconds),
)

mapObjectState.isValid

Placemark

PlacemarkState reaches the presentation objects and the scale function:

val placemarkState = rememberPlacemarkState(point)

LaunchedEffect(Unit) {
    placemarkState.setScaleFunction(listOf(PointF(10f, 0.5f), PointF(16f, 1f)))
    placemarkState.useCompositeIcon().setIcon("badge", badgeImage, IconStyle(zIndex = 1f))
    placemarkState.text.text = "Red Square"
}

geometry, direction and isDragging are observable properties of the state:

val placemarkState = rememberPlacemarkState(point)

Placemark(state = placemarkState, icon = icon, draggable = true)

if (placemarkState.isDragging) {
    Text("Dragging from ${placemarkState.geometry}")
}

Polyline segment colours

A polyline is coloured through a palette. setPaletteColor(colorIndex, color) defines the colour stored under an index, and setStrokeColors(paletteIndices) assigns one of those indexes to every segment — a polyline of n points has n − 1 segments. By default every segment uses palette index 0, which is what the strokeColor parameter sets.

getStrokeColor(segmentIndex) returns the palette index of a segment, not its colour; resolve it with getPaletteColor(colorIndex).

polylineState.setPaletteColor(0, Color.Red)
polylineState.setPaletteColor(1, Color.Green)
polylineState.setPaletteColor(2, Color.Blue)
polylineState.setStrokeColors(listOf(0, 1, 2))

PolylineState also selects and hides parts of the line, and adds arrows:

polylineState.select(Color.Blue, subpolyline)
polylineState.hide(subpolyline)

polylineState.addArrow(
    position = PolylinePosition(segmentIndex = 2, segmentPosition = 0.0),
    length = 12f,
    fillColor = Color.Black,
)

Polygon pattern

polygonState.setPattern(animatedHatch, scale = 1f)
polygonState.resetPattern()

Collections

collectionState.setPlacemarksScaleFunction(listOf(PointF(10f, 0.5f), PointF(16f, 1f)))

collectionState.traverse(visitor)

Clustering

Clustering builds a ClusterizedPlacemarkCollection and reclusters when its groups change. Each ClusterGroup has its own placemark icon; the cluster icon is shared.

@Composable
fun MapScreen() {
    val cameraPositionState = rememberCameraPositionState { position = startPosition }
    val pinRed = imageProvider(Res.drawable.pin_red)
    val clusterIcon = imageProvider(Res.drawable.cluster)

    YandexMap(
        cameraPositionState = cameraPositionState,
        modifier = Modifier.fillMaxSize(),
    ) {
        Clustering(
            group = ClusterGroup(points = redPoints, icon = pinRed),
            icon = clusterIcon,
            config = ClusterizingConfig(clusterRadius = 60.0, minZoom = 15),
            onItemTap = { item -> true },
            onClusterTap = { cluster -> true },
        )
    }
}
Clustering(
    groups = persistentListOf(
        ClusterGroup(points = redPoints, icon = pinRed),
        ClusterGroup(points = greenPoints, icon = pinGreen),
        ClusterGroup(points = yellowPoints, icon = pinYellow),
    ),
    icon = clusterIcon,
)
Clustering(
    group = ClusterGroup(
        placemarks = pois.map { ClusterItem(geometry = it.point, data = it) }.toPersistentList(),
        icon = pinRed,
        text = "POI",
    ),
    icon = clusterIcon,
    onItemTap = { item ->
        val poi = item.data as? Poi ?: return@Clustering false
        open(poi)
        true
    },
)

groups is an ImmutableList on purpose: Compose can then skip recomposition when the list has not changed, and reclustering is expensive.

Cluster icons can also be composable content — see Image resources.

Controller API

With the controller overload there is no composition of map objects: MapControllerEffect gives you the MapWindow once it exists, and you use the wrapper API directly.

@Composable
fun MapScreen() {
    val mapController = rememberYandexMapController()
    val pinGreen = imageProvider(Res.drawable.pin_green)
    MapControllerEffect(mapController) { mapWindow ->
        mapWindow.map.mapObjects.addPlacemark().apply {
            geometry = placemarkGeometry
            setIcon(pinGreen)
        }
    }
    YandexMap(
        controller = mapController,
        modifier = Modifier.fillMaxSize(),
    )
}
MapControllerEffect(mapController) { mapWindow ->
    mapWindow.map.mapObjects.addCircle(circleGeometry)
}
MapControllerEffect(mapController) { mapWindow ->
    mapWindow.map.mapObjects.addPolyline(polylineGeometry)
}
MapControllerEffect(mapController) { mapWindow ->
    mapWindow.map.mapObjects.addPolygon(polygonGeometry)
}

Clustering is the same as in the wrapper, listeners included:

@Composable
fun MapScreen() {
    val mapController = rememberYandexMapController()
    val clusterIcon = imageProvider(Res.drawable.cluster)
    val pinRed = imageProvider(Res.drawable.pin_red)

    val clusterTapListener = remember { ClusterTapListener { true } }
    val itemTapListener = remember { MapObjectTapListener { _, _ -> true } }
    val clusterListener = remember(clusterIcon) {
        ClusterListener { cluster ->
            cluster.appearance.setIcon(clusterIcon)
            cluster.appearance.setText("${cluster.size}")
            cluster.addClusterTapListener(clusterTapListener.asWeakRef())
        }
    }

    MapControllerEffect(mapController) { mapWindow ->
        val collection = mapWindow.map.mapObjects
            .addClusterizedPlacemarkCollection(clusterListener.asWeakRef())
        points.forEach { point ->
            collection.addPlacemark().apply {
                geometry = point
                setIcon(pinRed)
                addTapListener(itemTapListener.asWeakRef())
            }
        }
        collection.clusterPlacemarks(clusterRadius = 60.0, minZoom = 15)
    }

    YandexMap(
        controller = mapController,
        modifier = Modifier.fillMaxSize(),
    )
}

remember the listeners

Subscriptions are weak. A listener created inline in a composition is collected on the next recomposition and stops firing — remember it, or keep it in whatever owns the screen.