Skip to content

Geolocation

MapKit hands out three kinds of location source and one map layer that draws the result.

What it is
createLocationManager() the device's own positioning
createLocationSimulator(polyline) replays a route
createDummyLocationManager() positions your code pushes in
createUserLocationLayer(mapWindow) the arrow, pin and accuracy circle on the map

Permissions are yours to request

The wrapper does not ask for location permissions — do it the platform way before subscribing, or the manager reports LocationStatus.NOT_AVAILABLE.

Subscribing to positions

class LocationController(private val mapKit: MapKit) {

    private val locationManager = mapKit.createLocationManager()

    private val locationListener = LocationListener(
        onLocationUpdated = { location -> show(location.position) },
        onLocationStatusUpdated = { status ->
            if (status == LocationStatus.NOT_AVAILABLE) showUnavailable()
        },
    )

    fun start() {
        locationManager.subscribeForLocationUpdates(
            subscriptionSettings = SubscriptionSettings(
                useInBackground = UseInBackground.DISALLOW,
                purpose = LocationPurpose.GENERAL,
            ),
            locationListener = locationListener.asWeakRef(),
        )
    }

    fun stop() {
        locationManager.unsubscribe(locationListener.asWeakRef())
    }
}

purpose tells MapKit what the position is for — GENERAL, AUTOMOTIVE_NAVIGATION, PEDESTRIAN_NAVIGATION, BICYCLE_NAVIGATION, SCOOTER_NAVIGATION, STATIC_DISPLAY_LOCATION — and useInBackground whether it may keep working while the application is in the background.

For a single fix instead of a stream:

locationManager.requestSingleUpdate(locationListener.asWeakRef())

suspend() and resume() pause and continue the subscriptions without dropping them.

What a position carries

data class Location(
    val position: Point,
    val accuracy: Double?,
    val altitude: Double?,
    val altitudeAccuracy: Double?,
    val heading: Double?,
    val speed: Double?,
    val indoorLevelId: String?,
    val absoluteTimestamp: Instant,
    val relativeTimestamp: Duration,
)

Everything MapKit may not know is nullable. absoluteTimestamp is wall-clock time, relativeTimestamp is monotonic time since the device booted, which is what you compare positions by.

The last position MapKit received, without subscribing at all:

val last: Location? = lastKnownLocation()

Drawing the user on the map

class UserLocationController(mapKit: MapKit, mapWindow: MapWindow) {

    private val layer = mapKit.createUserLocationLayer(mapWindow)

    private val tapListener = UserLocationTapListener { point -> openDetails(point) }

    private val objectListener = UserLocationObjectListener(
        onObjectAdded = { view ->
            view.pin.setIcon(pinImage)
            view.arrow.setIcon(arrowImage)
            view.accuracyCircle.fillColor = Color.fromArgb(0x334CAF50)
        },
        onObjectRemoved = { },
        onObjectUpdated = { _, _ -> },
    )

    init {
        layer.isVisible = true
        layer.isHeadingModeActive = true
        layer.setTapListener(tapListener.asWeakRef())
        layer.setObjectListener(objectListener.asWeakRef())
    }
}

UserLocationView is three map objects — arrow, pin and accuracyCircle — so everything on Map objects applies to them. onObjectAdded is where they are styled; MapKit creates them lazily, after the first fix.

Anchoring

Anchor mode keeps the user's marker at a fixed point of the screen and moves the map under it, which is what navigation looks like. Two anchors are given: one for the normal state, one while the map follows the course.

layer.setAnchor(
    anchorNormal = PointF(width * 0.5f, height * 0.5f),
    anchorCourse = PointF(width * 0.5f, height * 0.75f),
)
layer.isAutoZoomEnabled = true

resetAnchor() leaves the mode; isAnchorEnabled reports whether it is on. Any camera movement by the user turns it off, so re-arm it when you want it back.

layer.cameraPosition is the position the layer would move the camera to, or null while there is no fix.

Feeding the layer from another source

layer.setSource(simulator.asLocationManager().toLocationViewSource())
layer.setDefaultSource()

Simulating a route

The simulator replays a Polyline — for demos, for screenshots, and for tests that must not depend on where the device is.

val route = Polyline(routePoints)
val simulator = mapKit.createLocationSimulator(route)

val settings = listOf(
    SimulationSettings(
        geometry = route,
        locationSettings = LocationSettings(
            provideSpeed = true,
            speed = 20.0,
            provideAccuracy = true,
            accuracy = Range(from = 3.0, to = 5.0),
        ),
    ),
)

simulator.asLocationManager().resume()
simulator.startSimulation(settings)

A simulator is created suspended

MapKit derives LocationSimulator from LocationManager, and startSimulation does not resume it. Without asLocationManager().resume() the simulation runs, isActive stays false and nothing reaches the subscribers.

asLocationManager() is what makes subscribeForLocationUpdates, requestSingleUpdate, unsubscribe, suspend and resume reachable — Kotlin cannot express MapKit's inheritance across two platforms, so the relation is spelled as a conversion. The result is a plain LocationManager, so it also goes into MapKit.setLocationManager() or toLocationViewSource().

private val simulatorListener = LocationSimulatorListener { println("route finished") }

simulator.subscribeForSimulatorEvents(simulatorListener.asWeakRef())

val progress = simulator.polylinePosition()

simulator.stopSimulation()

polylinePosition() is where along the route the simulation currently is.

speed on the simulator is deprecated

MapKit moved it into LocationSettings. startSimulation replaces settings wholesale, so a value assigned to simulator.speed beforehand is overwritten; set LocationSettings.speed together with provideSpeed = true on every SimulationSettings instead.

What LocationSettings controls

Each provideX flag decides whether the field is reported at all, and the value next to it says what to report:

LocationSettings(
    provideAccuracy = true,
    accuracy = Range(from = 3.0, to = 5.0),
    provideSpeed = true,
    speed = 20.0,
    provideHeading = true,
    headingError = Range(from = 0.0, to = 2.0),
    locationTimeInterval = TimeInterval(from = 1.seconds, to = 2.seconds),
)

LocationSettings.fineSettings() and LocationSettings.coarseSettings() are MapKit's presets.

Pushing positions in by hand

DummyLocationManager reports exactly what you give it — useful for tests and for replaying a recorded track at your own pace.

val dummy = mapKit.createDummyLocationManager()

dummy.asLocationManager().subscribeForLocationUpdates(
    subscriptionSettings = SubscriptionSettings(UseInBackground.DISALLOW, LocationPurpose.GENERAL),
    locationListener = locationListener.asWeakRef(),
)

dummy.setLocation(
    location = Location(
        position = Point(55.751225, 37.629540),
        accuracy = 5.0,
        altitude = null,
        altitudeAccuracy = null,
        heading = 0.0,
        speed = 0.0,
        indoorLevelId = null,
        absoluteTimestamp = Clock.System.now(),
        relativeTimestamp = 0.seconds,
    ),
    quality = DummyLocationQuality.HIGH,
)

As with the simulator, asLocationManager() is what opens the subscription members.

Replacing the global manager

MapKit.setLocationManager() makes every part of MapKit — including UserLocationLayer — use your source instead of the device's:

mapKit.setLocationManager(simulator.asLocationManager())

mapKit.resetLocationManagerToDefault()

In Compose

YandexMap has an overload that owns the layer, so nothing has to be created by hand:

@Composable
fun MapScreen() {
    val locationState = rememberUserLocationState()
    YandexMap(
        locationState = locationState,
        locationConfig = UserLocationConfig(
            isVisible = true,
            isHeadingEnabled = true,
            isAutoZoomEnabled = true,
            pin = UserLocationConfig.LocationIcon(image = pinImage),
            arrow = UserLocationConfig.LocationIcon(image = arrowImage),
            accuracy = UserLocationConfig.LocationAccuracy(
                fillColor = Color(0x334CAF50),
            ),
        ),
        modifier = Modifier.fillMaxSize(),
    )
}

locationState.cameraPosition follows UserLocationLayer.cameraPosition.