Вступление
Приветствую! Это вторая часть по разработке библиотеки для работы с Yandex Mapkit на Kotlin Multiplatform. В предыдущей я рассказывал про саму разработку библиотеки, которая будет интересна больше интересующимся созданием своей обёртки над каким-либо SDK.
В этой статье я расскажу про применение этой библиотеки при написании приложения на Compose Multiplatform, хотя есть используется в android-only приложении, она также применима. Можно рассматривать эту библиотеку как расширенный интероп MapView с UI на Compose для Android таргета, а можно как добавление поддержки Yandex MapKit SDK в common код с модулем для интеграции в Compose UI для Android/iOS приложений, всё зависит от вашего проекта.
Примечание: библиотека построена для использования вместе с враппером. Все объекты имеют пакет не com.yandex.mapkit
, а ru.sulgik.mapkit
. Подробнее про работу враппера можно прочитать в первой части.
Добавление зависимостей
Первым делом нужно добавить сам траппер и его реализацию для работы с Compose.
plugins {
kotlin("multiplatform") version "2.0.20"
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("ru.sulgik.mapkit:yandex-mapkit-kmp:0.1.0")
implementation("ru.sulgik.mapkit:yandex-mapkit-kmp-compose:0.1.0")
}
}
}
Теперь в нашем проекте доступны реализация неофициального мультиплатформенного Yandex MapKit SDK, однако этого недостаточно для того, чтобы завести приложение на iOS. Библиотека транзитивно не подключает pod официальной библиотеки, его необходимо добавить отдельно или в ваш Podfile
, или в cocoapods
блок (статья подразумевает, что вы уже имеете настроенную сборку проекта вместе с cocopods интеграцией).
plugins {
kotlin("multiplatform") version "2.0.20"
kotlin("native.cocoapods") version "2.0.20"
}
kotlin {
cocoapods {
pod("YandexMapsMobile") {
version = "4.7.0-lite"
}
}
// ...
}
0.1.0
– на данный момент последняя версия yandex-mapkit-kmp
, которая в самой себе использует последнюю версию Yandex MapKit SDK 4.7.0
на момент релиза. Пока присутствует поддержка только lite версии SDK, и то не полная.
Инициализация MapKit
Yandex MapKit работает только с полученным в кабинете разработчика API-ключом. Здесь не буду рассказывать как получить этот ключ, инструкция есть в официальной документации.
Однако в нашем приложении мы должны его применить. Для этого создаём в common коде функцию, которая вызывает установку ключа.
fun initMapKit() {
MapKit.setApiKey("<API_KEY>")
}
Дальше вызываем эту функцию из наиболее ранних точек входа в приложение, в том числе как рекомендует официальная документация. Для Android это onCreate
в Application
:
class MyApplication : Application {
override fun onCreate() {
super.onCreate()
initMapKit()
// ...
}
}
Для iOS можем выбрать наиболее удобную для нас.
@main
struct iOSApp: App {
init() {
AppKt.doInitMapKit()
}
// Your code here
}
Или же в AppDelegate
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
) -> Bool {
AppKt.doInitMapKit()
// ...
}
Выбираем API в зависимости от применения.
Карту могут применять в различных кейсах. От отображения единственной точки на карте, с которой даже нельзя взаимодействовать, до отображения сотен, тысяч и десятков тысяч самых разных объектов с постоянным обновлением и отложенной подгрузкой новых объектов с сервера.
Для первого примера совсем не важна большая производительность, проблемы не проявятся из-за маленькой нагрузки на SDK карты, для этого подойдёт, как я его назвал, States API, подробнее про него расскажу ниже.
Второй пример подразумевает активное взаимодействие и изменение карты, что при большом количестве постоянно модифицируемых объектов может вызвать проблемы с производительностью. Здесь подойдёт другой вариант применения, прозванный мной, Controller API.
Суть взаимодействия с каждым кардинально отличается и нельзя их использовать вместе, с маленькой оговоркой для States API.
Самый простой способ. States API
Идейным вдохновителем этого API стала библиотека Google Maps для работы с Compose UI. Суть – использование compose runtime для работы с состоянием карты, изменением параметров и даже добавлением объектов на карту с помощью composable функций.
val startPosition = CameraPosition(/* */)
@Composable
fun MapScreen() {
rememberAndInitializeMapKit().bindToLifecycleOwner() // if is not called earlier
val cameraPositionState = rememberCameraPositionState { position = startPosition }
YandexMap(
cameraPositionState = cameraPositionState,
modifier = Modifier.fillMaxSize()
)
}
rememberAndInitializeMapKit().bindToLifecycleOwner()
важен, поскольку на Android требуется отдельной инциализации MapKit, в том числе происходит подключение нативной библиотеки. А привязка к жизненному циклу необходима для корректной работы MapKit. Всё это требования самого SDK, подробнее в документации.
Теперь наша карта отображается в приложении и сразу происходит переход к стартовой позиции, заданной нами
Добавляем объекты
Для отображения доступны 4 типа объектов. Приведу их вместе с composable аналогами
PlacemarkMapObject
–Placemark
CircleMapObject
–Circle
PolygonMapObject
–Polygon
PolylineMapObject
–Polyline
Есть и другие наследники от
MapObject
(как ClusterizedPlacemarkCollection) однако они не будут рассмотрены в контексте этой статьи.
Для добавления их на карту с применением State API много стараний не нужно. Просто вызываем composable функцию внутри контента YandexMap
.
val placemarkGeometry = Point(/* */)
@Composable
fun MapScreen() {
val cameraPositionState = rememberCameraPositionState { position = startPosition }
YandexMap(
cameraPositionState = cameraPositionState,
modifier = Modifier.fillMaxSize(),
) {
val imageProvider = imageProvider(Res.drawable.pin_red) // Using compose multiplatform resources
Placemark(
state = rememberPlacemarkState(placemarkGeometry),
icon = imageProvider,
)
}
}
Всё понятно и очевидно. Здесь используется compose runtime для добавления объектов на карту. Из ограничений – нельзя в контексте @YandexMapComposable
, которой помечен контент YandexMap
, вызывать composable, которые обращаются к компонентам из compose ui модуля.
Ресурсы
В блоке кода выше можно заметить вызов imageProvider()
, он необходим для получения изображения, которое будет использоваться как иконка для Placemark.
В оригинальном SDK используется UIImage для iOS и BitMap, Drawable, File и др. для Android. Если есть возможность и желание, можете прокидывать их из нативного кода преобразованием ImageProvider.fromBitmap()
, ImageProvider.fromDrawable()
для использования в общем коде.
Однако моя библиотека предоставляет возможнось для использования готовых решений мультиплатформенных ресурсов.
Compose Multiplatform Resources – часть модуля
yandex-mapkit-kmp-compose
Moko resources – часть модуля
yandex-mapkit-kmp-moko
иyandex-mapkit-kmp-moko-compose
. Рассматривать в статье не будем, можно прочитать в документации
Для использования с Compose Multiplatform Resources отдельно стараться не нужно. Просто вызвать imageProvider(DrawableResource), куда передаём наш ресурс
val imageProvider = imageProvider(Res.drawable.pin_red)
Подкапотная мультиплатформенная реализация весьма интересная, но она позволяет не задумываться над использованием ресурсов в нашей карты.
Composable как ImageProvider
Но наиболее крутая возможность это, пока экспериментальная, функция – использование composable контент как иконку. Т.е можно в рантайме модифицировать отображаемые на карте контент, используя привычный compose ui.
В официальном SDK для Android и iOS есть возможность устанавливать вьюшку как иконку. Однако под капотом преобразования в ImageProvider
это лишь рендеринг прямиком в картинку и сохранение самой картинки. Это накладывает ограничения, которые требует поиск способов обхода. Рассмотрим создание такого ImageProvider
@Composable
fun MapScreen() {
var clicksCount by remember { mutableStateOf(0) }
val density = LocalDensity.current
val contentSize = with(density) { DpSize(75.dp, 10.dp + 12.sp.toDp()) }
val clicksImageProvider = imageProvider(size = contentSize, clicksCount) {
Box(
modifier = Modifier
.background(Color.LightGray, MaterialTheme.shapes.medium)
.border(
1.dp,
MaterialTheme.colorScheme.outline,
MaterialTheme.shapes.medium
)
.padding(vertical = 5.dp, horizontal = 10.dp)
) {
Text("clicks: $clicksCount", fontSize = 12.sp)
}
}
}
Теперь разберёмся что здесь происходит
clicksCount
- счётчик нажатий, изменяемый по клику наPlacemark
,clickable
нет в этом коде, поскольку хочу напомнить, что этот контент не будет отрисован как часть интерфейса, он будет сразу рендерится вBitmap
contentSize
– игнорируется на Android. Применим только на iOS, поскольку рендеринг там происходит через весьма костыльный метод. Описывает размер изображения, конвертируемого в Bitmap. Этот способ будет удалён в будущих версиях библиотеки, пока используется как есть.imageProvider(size = contentSize, clicksCount) { /* */ }
. size уже разобрали выше, а вотclicksCount
передаётся сюда для того, чтобы вызвать ререндер в bitmap при изменение этого поля
Пока библиотека в активной разработке, функционал доступен лишь в таком виде, но вы всегда можете контрибьютить в эту библиотеку, чтобы улучшить её.
События
Для обработки нажатий на объекты ничего особенного не нужно, просто передаём callback в наш Placemark
. Boolean, который мы возвращаем, – это индикатор того, что мы обработали этот тап.
Placemark(
icon = clicksImageProvider,
state = rememberPlacemarkState(placemarkGeometry),
onTap = {
clicksCount++
true
}
)
Доступ к оригинальному экземпляру Map
В @YandexMapComposable
контексте можно получить доступ к созданному в YandexMap
экземпляру Map
. Можно применять его в отдельных случаях, когда States API не предоставляет желаемого функционала.
@Composable
fun MapScreen() {
YandexMap(/* */) {
MapEffect { map: Map ->
// use map here
}
}
}
Полный доступ к Map. Controller API
Если планируется расширенное взаимодействие с картой, то следует выбрать этот способ. Суть – полный доступ к экземпляру карты и управление только через через негу.
@Composable
fun MapScreen() {
//...
val mapController = rememberYandexMapController()
MapControllerEffect(mapController) { mapWindow ->
mapWindow.map.move(startPosition)
mapWindow.map.isZoomGesturesEnabled = true
}
YandexMap(
controller = mapController,
modifier = Modifier.fillMaxSize(),
)
}
Мы не задействуем compose runtime и можем эффективнее использовать карту. YandexMap
просто создаёт карту, отрисовывает её и передаёт созданный экземпляр карт в YandexMapController
.
YandexMapController.mapWindow
является null пока он не будет создан YandexMap. Для гарантии non-null и используетсяMapControllerEffect
.
Заключение
Библиотека позволяет упростить процесс интеграции Yandex MapKit SDK в проект на Compose, в том числе и мультиплтаформенный. На данный момент библиотека в активной разработке и часть функционала пока не доступна (хотя можно выполнить toNative()
, если вы пишете под одну платформу, подробнее можно прочитать в первой части).
Это ещё не конец цикла статей, будут ещё несколько статей про under the hood этой библиотеки. А пока можете прочитать первую часть, чтобы понять как устроен враппер в этой библиотеки.
Я никак не связан с Яндекс. Я лишь автор библиотеки, позволяющий использовать их разработку, MapKit SDK, в "экосистеме" KMP проектов. Все api key, необходимые для работы с SDK получаются как и с официальной библиотекой, на сайте Яндекса. Я не претендую ни на ваши api ключи, ни на деньги с покупки тарифов Яндексу. Даже возможно, что эта библиотека привлечет Яндексу некоторое количество клиентов, заинтересованных в разработке под KMP. Контакты для связи: Владимир @vollllodya