Обзор обёртки¶
yandex-mapkit-kmp — модуль, который делает MapKit доступным из общего кода. Он повторяет пакеты,
типы и члены MapKit, поэтому
официальная документация по-прежнему описывает, что делает
конкретный член; эта страница — про то, что обёртка добавляет сверху.
Она ничего не рисует¶
Модуль оборачивает API, а не view. MapView вы создаёте на платформе ровно так, как требует
официальный SDK, преобразуете его и передаёте в общий код.
Пример
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)
}
Чтобы саму view создавать из общего кода, подключите
yandex-mapkit-kmp-compose.
Пересечение границы¶
Переход между общим и нативным записывается ровно двумя именами, и оба живут только в платформенных исходниках:
public fun NativePoint.toCommon(): Point
public fun Point.toNative(): NativePoint
commonMain никогда не видит нативный тип — именно поэтому один и тот же общий код компилируется
под обе платформы. У всего, что отдаёт MapKit, есть toCommon(), а у всего, что держит обёртка, —
toNative(), поэтому спуститься к платформенному SDK за тем, чего обёртка не покрывает, можно
всегда.
Три вида типов¶
Понимание того, что именно вы держите в руках, объясняет поведение.
Value-типы — неизменяемые данные: Point, CameraPosition, IconStyle, Animation,
Location, LogMessage. В общем коде это data class, сравниваются по полям.
Хендлы стоят за живым объектом MapKit, которым вы управляете: Map, MapWindow,
PlacemarkMapObject, LocationManager, TrafficLayer, Icon. Они держат нативный объект и
переадресуют ему каждый вызов.
Слушатели — абстрактные классы с фабричной лямбдой того же имени:
val tapListener = MapObjectTapListener { mapObject, point -> true }
val inputListener = InputListener(
onMapTap = { _, point -> println("tap at $point") },
onMapLongTap = { _, point -> println("long tap at $point") },
)
Слушатели и WeakRef¶
MapKit не удерживает слушателей: он держит их в слабой коллекции и удаляет по идентичности. Поэтому
каждая подписка в обёртке принимает WeakRef<Listener>, а сильную ссылку держит вызывающий код.
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())
}
}
Держите слушателя сами
map.addInputListener(InputListener { /* ... */ }.asWeakRef()) скомпилируется и никогда не
сработает: слушателя никто не держит, и он будет собран. Положите его в поле того, кому
принадлежит подписка, — контроллера, Compose-узла, модели экрана.
Отписаться можно новым WeakRef вокруг того же слушателя: toNative() в обёртке всю жизнь
возвращает один и тот же нативный экземпляр, а MapKit сравнивает именно по нему.
У WeakRef также есть get(): T? и withValue { }, чтобы прочитать значение обратно.
Идентичность хендлов¶
Новая обёртка создаётся каждый раз, когда нативный объект пересекает границу, поэтому метка, которую вам отдал слушатель нажатия, — никогда не тот же экземпляр обёртки, который вы добавляли.
Поэтому хендлы сравниваются по объекту за ними: два хендла равны, когда у них одинаковый тип и они
оборачивают один и тот же нативный объект, а hashCode() — это хеш обёрнутого объекта, так что
хендл годится в качестве ключа Map или элемента Set.
Пример
val placemark = collection.addPlacemark()
val tapListener = MapObjectTapListener { tapped, _ ->
tapped == placemark
}
Тип участвует в сравнении намеренно. MapObject.toCommon() откатывается к базовой обёртке для
неизвестного обёртке подтипа, так что простой MapObject и PlacemarkMapObject могут стоять за
одним и тем же объектом; сделать их равными означало бы сломать симметричность equals.
Оба члена безопасны на невалидном хендле (isValid == false), где любой другой член бросает
исключение: они сравнивают ссылки и никогда не заходят в MapKit.
Value-типы — Point, CameraPosition, IconStyle и остальные data class — по-прежнему
сравниваются по полям.
OfflineCacheError на iOS сравнивается по значению
Обёртывание одного и того же нативного объекта всегда делает хендлы равными, но у одного типа
это не единственный способ оказаться равными. offline_cache.OfflineCacheError оборачивает
NSError на iOS и runtime.Error на Android, а NSError переопределяет isEqual: и hash со
значимой семантикой. Две разные ошибки с одинаковыми domain, code и userInfo поэтому окажутся
равны на iOS и не равны на Android. NSError неизменяем, так что его хеш не «уплывает»;
считайте равенство OfflineCacheError признаком «та же самая ошибка», а не «тот же объект».
Время жизни и isValid¶
MapKit инвалидирует свои объекты, когда исчезает то, чему они принадлежат: объект карты, удалённый
из коллекции; слой, снятый с карты; вообще всё после MapKit.onTerminate(). Обращение к невалидному
объекту бросает исключение.
Хендлы выставляют это как isValid, и проверку стоит делать везде, где объект может пережить карту:
if (placemark.isValid) {
placemark.geometry = point
}
Исключение — equals и hashCode: они не заходят в MapKit и остаются пригодными.
Kotlin вместо голых значений¶
kotlin.time.Durationдля длительностей,kotlin.time.Instantдля моментов времени.- Nullable-типы вместо значений-заглушек:
poiLimit: Int?,focusRect: ScreenRect?,lastKnownLocation(): Location?. Color— value-класс поверх ARGB, создаётся черезColor.fromArgb(argb), читается черезtoArgb(). Compose-модуль принимаетandroidx.compose.ui.graphics.Colorи конвертирует сам.- Value-классы
LatitudeиLongitudeзаPoint, чтобы порядок аргументов нельзя было молча перепутать. - Перечисления вместо целочисленных констант, и без ветки по умолчанию: неизвестное нативное значение бросает исключение, а не превращается молча во что-то правдоподобное.
Потоки¶
MapKit вызывает колбэки на главном потоке и ожидает, что им управляют оттуда же. Обёртка не добавляет своей многопоточности: колбэк приходит на том потоке, который выбрал MapKit, а вызов уходит напрямую. Считайте любой объект обёртки предназначенным только для главного потока, если MapKit не говорит иного.
Что дальше¶
- Что обёрнуто — покрытие по пакетам.
- Карта и камера, Объекты карты, Объекты базовой карты.
- Геолокация, Слои и тайлы, Офлайн-карты и хранилище, Runtime.