Вступление

Для создания динамичных и визуально привлекательных карт иногда требуется отображать изображения, например, аватары пользователей, загружаемые с сервера. В данной статье мы рассматриваем, как загрузить изображение через Coil и отобразить его на карте при помощи нашего SDK для Yandex MapKit.

В этой статье мы расскажем, как интегрировать библиотеку Coil для загрузки и отображения изображений на карте в мультиплатформенном приложении, использующем Kotlin-first SDK для Yandex MapKit. Мы подробно рассмотрим реализацию компонента CoilPlacemark, а также особенности создания мультиплатформенного метода-расширения, передачу ключа для перерендеринга и экспериментальный статус API imageProvider.

Данная статья использует библиотеку от сообщества Yandex MapKit KMP Compose, которая разработана для создания мультиплатформенных приложений с поддержкой компоновки на Android и iOS.

Навигация по статьям

  1. Как я писал враппер для Яндекс Карт на KMP. Часть 1 – расширит понимание и особенности работы библиотеки враппера для использования в мультиплатформенного проекта

  2. Используем Yandex MapKit с Compose Multiplatform. Часть 2рекомендуется к прочтению. Разбираются нюансы интеграции нативных карт от Яндекса в Compose Multiplatform интерфейс

  3. Coil и Yandex MapKit KMP: рендеринг изображений на карте. Часть 3 – текущая статья

Настройка зависимостей

Используйте следующий файл gradle/libs.versions.toml для настройки зависимостей:

[versions]
ktor = "3.1.1"
coil = "3.1.0"
yandex-mapkit-kmp-compose = "0.2.0"

[libraries]
ktor-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
yandex-mapkit-kmp-compose = { module = "ru.sulgik.mapkit:yandex-mapkit-kmp-compose", version.ref = "yandex-mapkit-kmp-compose" }

Также обновите файл сборки для различных платформ (например, sample/composeApp/build.gradle.kts) следующим образом:

androidMain.dependencies {
    implementation(libs.ktor.okhttp)
}
iosMain.dependencies {
    implementation(libs.ktor.darwin)
}
commonMain.dependencies {
    implementation(libs.coil.compose)
    implementation(libs.coil.ktor)
    implementation(libs.yandex.mapkit.kmp.compose)
}

Настройка API ключа

Для работы MapKit необходимо инициализировать библиотеку с вашим API ключом. Добавьте функцию инициализации в общий модуль:

// В общем модуле
fun initMapKit() {
    MapKit.setApiKey("<API-KEY>")
}

Вызовите эту функцию в точке входа для каждой платформы:

Android:

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        initMapKit()
    }
}

iOS (Swift):

@main
struct iOSApp: App {
    init() {
        AppKt.doInitMapKit()
    }
    // Ваш остальной код
}

Дополнительную информацию смотрите в документации.

Реализация CoilPlacemark

Ниже представлена полная реализация компонента CoilPlacemark. Обратите внимание, что функция imageProvider является экспериментальным API — подробности можно найти по этой ссылке.

@OptIn(YandexMapsComposeExperimentalApi::class)
@Composable
fun CoilPlacemark() {
    val placemarkState = rememberPlacemarkState(
        geometry = Point(59.939095, 30.338655),
    )
    val painter = rememberAsyncImagePainter(
        ImageRequest.Builder(LocalPlatformContext.current)
            .data("https://api.dicebear.com/9.x/icons/png")
            .allowHardware(false)
            .build()
    )

    val imageProvider = imageProvider(
        size = DpSize(40.dp, 40.dp),
        key1 = painter.state.collectAsState().value,
    ) {
        Image(
            painter = painter,
            contentDescription = null,
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape),
            contentScale = ContentScale.Crop,
        )
    }
    Placemark(
        state = placemarkState,
        icon = imageProvider,
    )
}

Описание особенностей

Создание мультиплатформенной функции-расширения

Для управления использованием аппаратного ускорения при загрузке изображений через Coil создаём мультиплатформенный метод-расширения ImageRequest.Builder.allowHardware. Это важно, так как при включенном аппаратном ускорении на Android может возникнуть ошибка:

java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps

Чтобы избежать этой ошибки, в Android‑реализации необходимо отключать аппаратное ускорение (вызывать allowHardware(false)), а для iOS можно оставить поведение по умолчанию.

По моему, возможность выключить аппаратное ускорение прям в общем коде должна предоставляться из коробки.

Общий код

// Реализация мультиплатформенного метода-расширения для ImageRequest.Builder
expect fun ImageRequest.Builder.allowHardware(enabled: Boolean): ImageRequest.Builder

Реализация для Android

actual fun ImageRequest.Builder.allowHardware(enabled: Boolean): ImageRequest.Builder {
    return allowHardware(enabled)
}

Реализация для iOS

actual fun ImageRequest.Builder.allowHardware(enabled: Boolean): ImageRequest.Builder {
    return this
}

Передача ключа для перерендеринга

Функция imageProvider использует переданный ключ, чтобы определить, когда должен произойти перерендер. Это работает аналогично ключу в функции remember в Compose: компонент будет перерендерен только при изменении значения ключа. В нашем примере в качестве ключа используется значение из painter.state.collectAsState().value, что означает, что обновление изображения инициируется только при изменении состояния загрузки.

Если имеется другие данные, которые влияют на отрисовку imageProvider, то их стоит передать другими ключами

Экспериментальный статус API imageProvider

API imageProvider является экспериментальным по следующим причинам:

  • Его реализация содержит ограничения при использовании composable контента, например, она берёт однократный снимок контента для создания изображения.

  • Поведение функции отличается на разных платформах: на Android параметр size игнорируется, в то время как на iOS он используется для определения размеров снимка.

  • API всё ещё находится в разработке, и его возможности могут быть улучшены с участием сообщества.

Мы призываем всех заинтересованных присоединиться к обсуждению и внести свой вклад в улучшение работы imageProvider с composable контентом, чтобы обеспечить более согласованное и надёжное поведение на обеих платформах. Подробнее в документации

Использование CoilPlacemark

Для демонстрации применения созданного компонента рассмотрим функцию MapScreen. Здесь происходит инициализация Yandex MapKit с вызовом rememberAndInitializeMapKit().bindToLifecycleOwner(), который:

  • Инициализирует нативные компоненты MapKit для Android.

  • Привязывает их к жизненному циклу, предотвращая утечки памяти и обеспечивая стабильную работу карты.

// Определяем стартовую позицию камеры.
private val startPosition = CameraPosition(
    target = Point(59.938, 30.317),
    zoom = 10f
)

@Composable
fun MapScreen(modifier: Modifier = Modifier) {
    rememberAndInitializeMapKit().bindToLifecycleOwner()  // Инициализация и привязка к жизненному циклу

    val cameraPositionState = rememberCameraPositionState { 
        position = startPosition 
    }

    YandexMap(
        cameraPositionState = cameraPositionState,
        config = MapConfig(
            isNightModeEnabled = isSystemInDarkTheme(),
        ),
        modifier = modifier,
    ) {
        // Вызов компонента CoilPlacemark
        CoilPlacemark()
    }
}

Заключение

В данной статье мы рассмотрели, как интегрировать библиотеку Coil для загрузки и отображения изображений на карте в мультиплатформенном приложении с использованием Yandex MapKit SDK. Мы подробно описали:

  • Реализацию метода CoilPlacemark для отображения изображения на карте.

  • Создание мультиплатформенного метода-расширения ImageRequest.Builder.allowHardware

  • Передачу ключа в imageProvider, работающего аналогично ключу в функции remember для определения момента перерендеринга.

  • Экспериментальный статус imageProvider с его особенностями на Android и iOS, а также призыв к участию сообщества для улучшения API.

  • Необходимость вызова rememberAndInitializeMapKit().bindToLifecycleOwner() для корректной инициализации нативных компонентов MapKit и их привязки к жизненному циклу.

  • Пример использования CoilPlacemark в функции MapScreen.

Надеюсь, данный пример и подробное описание помогут вам успешно интегрировать новые возможности в ваше приложение. Если возникнут вопросы или предложения, оставляйте комментарии ниже!

Я никак не связан с Яндекс. Я лишь автор библиотеки, позволяющий использовать их разработку, MapKit SDK, в "экосистеме" KMP проектов. Все api key, необходимые для работы с SDK получаются как и с официальной библиотекой, на сайте Яндекса. Я не претендую ни на ваши api ключи, ни на деньги с покупки тарифов Яндексу. Даже возможно, что эта библиотека привлечет Яндексу некоторое количество клиентов, заинтересованных в разработке под KMP. Контакты для связи: Владимир @vollllodya