Skip to content
This site is a preview of pull request #1363.

Expressions in Kotlin

Layer properties in MapLibre are not plain values. Each property takes an expression: a formula that the map evaluates while it renders, per feature and per zoom level. The MapLibre Style Specification defines expressions as JSON arrays. MapLibre Compose provides a Kotlin DSL that builds them with type checking, in org.maplibre.compose.expressions.dsl.

The DSL uses Compose types when a Compose type matches the property: Dp for sizes, Color for colors, and DpOffset for offsets.

A constant value must also be an expression. Wrap a plain value in const:

App.kt
val state = rememberMapState {
val earthquakes =
rememberGeoJsonSource(
GeoJsonData.Uri("https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson")
)
CircleLayer(
id = "quakes-constant",
source = earthquakes,
radius = const(4.dp),
color = const(Color.Red),
)
}
MaplibreMap(state = state)

feature accesses the properties of the feature being rendered. The result is untyped, so convert it with functions such as asNumber() or asString(). Functions such as step and switch select an output from the value:

App.kt
val state = rememberMapState {
CircleLayer(
id = "quakes-by-magnitude",
source = earthquakes,
radius =
step(
input = feature["mag"].asNumber(),
fallback = const(4.dp),
4 to const(8.dp),
6 to const(16.dp),
),
color =
switch(
condition(test = feature["mag"].asNumber() gt const(5), output = const(Color.Red)),
fallback = const(Color.Yellow),
),
)
}
MaplibreMap(state = state)

The map evaluates this formula once per feature: each earthquake receives a radius and a color computed from its own magnitude.

zoom is the current zoom level. interpolate computes a smooth value between stops:

App.kt
val state = rememberMapState {
CircleLayer(
id = "quakes-by-zoom",
source = earthquakes,
radius =
interpolate(
type = exponential(2f),
input = zoom(),
5 to const(2.dp),
10 to const(8.dp),
),
)
}
MaplibreMap(state = state)

The filter parameter takes a boolean expression. The layer renders only the features for which it is true:

App.kt
val state = rememberMapState {
CircleLayer(
id = "large-quakes",
source = earthquakes,
filter = feature["mag"].asNumber() gt const(5),
)
}
MaplibreMap(state = state)

A feature property can be absent, so feature["place"] has the type Expression<AnyValue?>: the type is unknown and the value may be null. A nullable type argument follows Kotlin’s rule. Expression<StringValue?> may evaluate to null and Expression<StringValue> may not.

An assertion such as asString() aborts the expression for a feature without the property, and the map draws the property’s default instead. Where a null is acceptable, keep the nullable type: a text or image property renders a null as nothing, and eq, neq, and switch on a value handle it. Where a value is required, supply one with coalesce and a fallback, or with the fallback argument of an assertion such as asNumber(const(0f)).

App.kt
val state = rememberMapState {
SymbolLayer(
id = "quake-places",
source = earthquakes,
// A missing "place" renders as no text.
textField = feature["place"].cast<StringValue?>(),
)
CircleLayer(
id = "quakes-with-fallback",
source = earthquakes,
// A missing "mag" counts as 1.
radius = coalesce(feature["mag"].cast<FloatValue?>(), fallback = const(1f)).dp,
filter = feature["place"] neq nil(),
)
}
MaplibreMap(state = state)

nil is a null literal. MapLibre accepts it only where the expected type is unknown, such as in a comparison. To leave a layer property unset, pass Kotlin null to the layer.

Kotlin code outside an expression runs during composition and produces an expression. The expression runs inside the map renderer, once per feature. A Kotlin if selects which expression to build; an expression switch selects a value per feature. Use Kotlin for decisions that depend on app state, and expressions for decisions that depend on feature data or zoom.

The API reference documents the full set of expression functions under org.maplibre.compose.expressions.dsl.