Wrapper overview¶
yandex-mapkit-kmp is the module that makes MapKit reachable from common code. It mirrors MapKit's
packages, types and members, so the
official documentation keeps describing what a member does;
this page describes what the wrapper adds on top.
It does not draw anything¶
The module wraps the API, not the view. You create a MapView on the platform side exactly as the
official SDK asks, convert it, and hand the result to common code.
Example
fun setupMap(map: Map) {
map.move(
CameraPosition(Point(55.751225, 37.629540), zoom = 15f, azimuth = 0f, tilt = 0f),
)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MapKit.initialize(this)
setContentView(R.layout.activity_main)
val mapView = findViewById<com.yandex.mapkit.mapview.MapView>(R.id.map)
setupMap(mapView.mapWindow.map.toCommon())
}
}
fun setupMap(mapView: YMKMapView) {
setupMap(mapView.mapWindow.toCommon().map)
}
To have the view itself created from common code, add
yandex-mapkit-kmp-compose.
Crossing the boundary¶
Exactly two names spell every crossing, and both live in the platform source sets only:
public fun NativePoint.toCommon(): Point
public fun Point.toNative(): NativePoint
commonMain never sees a native type, which is what lets the same common code compile for both
platforms. Everything MapKit hands out has a toCommon(), and everything the wrapper holds has a
toNative(), so falling back to the platform SDK for something the wrapper does not cover is always
possible.
Three kinds of type¶
Knowing which kind you are holding explains its behaviour.
Value types are immutable data — Point, CameraPosition, IconStyle, Animation,
Location, LogMessage. They are data classes in common code and compare by their fields.
Handle types stand for a live MapKit object you drive — Map, MapWindow, PlacemarkMapObject,
LocationManager, TrafficLayer, Icon. They hold the native object and forward every call to it.
Listeners are abstract classes with a lambda factory of the same name:
val tapListener = MapObjectTapListener { mapObject, point -> true }
val inputListener = InputListener(
onMapTap = { _, point -> println("tap at $point") },
onMapLongTap = { _, point -> println("long tap at $point") },
)
Listeners and WeakRef¶
MapKit does not retain listeners: it keeps them in a weak collection and removes them by identity.
Every subscription in the wrapper therefore takes a WeakRef<Listener>, and the caller keeps the
strong reference.
class MapController(private val map: Map) {
private val inputListener = InputListener(
onMapTap = { _, point -> onTap(point) },
onMapLongTap = { _, _ -> },
)
init {
map.addInputListener(inputListener.asWeakRef())
}
fun dispose() {
map.removeInputListener(inputListener.asWeakRef())
}
}
Keep the listener alive yourself
map.addInputListener(InputListener { /* ... */ }.asWeakRef()) compiles and then never fires:
nothing holds the listener, so it is collected. Store it in a field of whatever owns the
subscription — a controller, a Compose node, a screen model.
Unsubscribing works with a fresh WeakRef around the same listener, because the wrapper's
toNative() returns one stored native instance per listener for its whole life, and MapKit compares
by that instance.
WeakRef also has get(): T? and withValue { } for reading the referent back.
Handle identity¶
A fresh wrapper is built every time a native object crosses the boundary, so the placemark a tap listener hands you is never the same wrapper instance you added.
Handles therefore compare by the object behind them: two handles are equal when they have the same
type and wrap the same native object, and hashCode() is the wrapped object's, so a handle works
as a Map key or in a Set.
Example
val placemark = collection.addPlacemark()
val tapListener = MapObjectTapListener { tapped, _ ->
tapped == placemark
}
The type is part of the comparison on purpose. MapObject.toCommon() falls back to the base wrapper
for a subtype the wrapper does not know, so a plain MapObject and a PlacemarkMapObject can stand
for the same object; making them equal would break the symmetry of equals.
Both members are safe on an invalidated handle (isValid == false), where every other member throws:
they compare references and never call into MapKit.
Value types — Point, CameraPosition, IconStyle and the rest of the data classes — keep
comparing by their fields.
OfflineCacheError compares by value on iOS
Wrapping the same native object always makes two handles equal, but on one type it is not the
only way to be equal. offline_cache.OfflineCacheError wraps an NSError on iOS and a
runtime.Error on Android, and NSError overrides isEqual: and hash with value semantics.
Two distinct errors carrying the same domain, code and userInfo therefore compare equal on
iOS and unequal on Android. NSError is immutable, so its hash never drifts; treat
OfflineCacheError equality as "the same failure", not as "the same object".
Lifetime and isValid¶
MapKit invalidates its objects when what they belong to goes away — a map object removed from its
collection, a layer removed from the map, everything after MapKit.onTerminate(). Touching an
invalidated object throws.
Handles expose that as isValid, and it is worth checking wherever an object can outlive the map:
if (placemark.isValid) {
placemark.geometry = point
}
equals and hashCode are the exception — they never call into MapKit and stay usable.
Kotlin instead of raw values¶
kotlin.time.Durationfor durations,kotlin.time.Instantfor timestamps.- Nullable types instead of sentinel values:
poiLimit: Int?,focusRect: ScreenRect?,lastKnownLocation(): Location?. Color, a value class over ARGB, built withColor.fromArgb(argb)and read withtoArgb(). The Compose module takesandroidx.compose.ui.graphics.Colorand converts it.LatitudeandLongitudevalue classes behindPoint, so the argument order cannot silently swap.- Enums instead of integer constants, with no default branch: an unknown native value throws rather than being silently mapped onto something plausible.
Threading¶
MapKit calls back on the main thread and expects to be driven from it. The wrapper adds no threading of its own: a callback arrives on the thread MapKit chose, and a call goes straight through. Treat every wrapper object as main-thread-only unless MapKit says otherwise.
What is next¶
- What is wrapped — package-by-package coverage.
- Map and camera, Map objects, Base map objects.
- Geolocation, Layers and tiles, Offline maps and storage, Runtime.