Using Yandex MapKit with Compose Multiplatform. Part 2

Introduction

Hello! This is the second part of the development libraries for working with Yandex Mapkit on Kotlin Multiplatform. In the previous one I talked about the library development itself, which will be more interesting for those interested in creating their own wrapper over any SDK.

In this article I will talk about the use of this library when writing an application on Compose Multiplatform, although it is used in an android-only application, it is also applicable. You can consider this library as an extended MapView interop with UI on Compose for Android target, or as adding support for Yandex MapKit SDK to common code with a module for integration into Compose UI for Android/iOS applications, it all depends on your project.

Note: The library is built to be used with a wrapper. All objects have a package not com.yandex.mapkit A ru.sulgik.mapkit . You can read more about the wrapper's work in the first part.

Adding dependencies

The first thing you need to do is add the trapper itself and its implementation to work with 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")
        }
    }
}

Now our project has an implementation of the unofficial multiplatform Yandex MapKit SDK, but this is not enough to start an application on iOS. The library does not transitively connect the pod of the official library, it must be added separately or to your Podfileor in cocoapods block (the article assumes that you already have a configured project build with cocopods integration).

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 – currently the latest version yandex-mapkit-kmpwhich itself uses the latest version of Yandex MapKit SDK 4.7.0 at the time of release. So far, only the lite version of the SDK is supported, and even that is not complete.

Initializing MapKit

Yandex MapKit only works with the data received in in the developer's office API key. I won't tell you how to get this key here, the instructions are in the official documentation.

However, in our application we must apply it. To do this, we create a function in the common code that calls for the key installation.

fun initMapKit() {
    MapKit.setApiKey("<API_KEY>")
}

Next, we call this function from the earliest entry points into the application, including as recommended by the official documentation. For Android, this is onCreate V Application:

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

For iOS we can choose the most convenient one for us.

@main
struct iOSApp: App {
    init() {
        AppKt.doInitMapKit()
    }
    // Your code here   
}

Or in AppDelegate

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
) -> Bool {
    AppKt.doInitMapKit()
    // ...
}

We select API depending on the application.

The map can be used in various cases. From displaying a single point on the map, which cannot even be interacted with, to displaying hundreds, thousands and tens of thousands of different objects with constant updating and delayed loading of new objects from the server.

For the first example, high performance is not important at all, problems will not appear due to the small load on the SDK of the card, for this, as I called it, States API is suitable, I will tell you more about it below.

The second example involves a lot of interaction and map modification, which can cause performance issues when there are a lot of objects being modified. Another use case that I've dubbed the Controller API is appropriate here.

The essence of interaction with each is fundamentally different and they cannot be used together, with a small caveat for the States API.

The easiest way. States API

The inspiration behind this API was Google Maps library for working with Compose UI. The essence is using compose runtime to work with the state of the map, changing parameters and even adding objects to the map using composable functions.

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() is important because Android requires separate initialization of MapKit, including the connection of the native library. And binding to the life cycle is necessary for the correct operation of MapKit. All these are requirements of the SDK itself, more details in the documentation.

Now our map is displayed in the application and the transition to the starting position specified by us occurs immediately.

Adding objects

There are 4 types of objects available for display. I will list them together with their composable analogues.

  • PlacemarkMapObjectPlacemark

  • CircleMapObjectCircle

  • PolygonMapObjectPolygon

  • PolylineMapObjectPolyline

  • There are other heirs from MapObject (How ClusteredPlacemarkCollection) however, they will not be discussed in the context of this article.

Adding them to the map using the State API doesn't require much effort. Just call the composable function inside the content. 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,
        )
    }
}

Everything is clear and obvious. Here compose runtime is used to add objects to the map. Of the limitations – it is impossible in the context @YandexMapComposable which the content is marked with YandexMapcall composable, which refers to components from the compose ui module.

Resources

In the code block above you can see a call imageProvider() it is needed to get the image that will be used as an icon for Placemark.

The original SDK uses UIImage for iOS and BitMap, Drawable, File, etc. for Android. If you have the opportunity and desire, you can pass them from the native code by converting ImageProvider.fromBitmap() , ImageProvider.fromDrawable() for use in shared code.

However, my library provides the opportunity to use ready-made solutions of multi-platform resources.

  1. Compose Multiplatform Resources – part of the module yandex-mapkit-kmp-compose

  2. Moko resources – part of the module yandex-mapkit-kmp-moko And yandex-mapkit-kmp-moko-compose. We will not consider it in the article, you can read it in documentation

To use with Compose Multiplatform Resources, you don't need to try separately. Just call imageProvider(DrawableResource), where we pass our resource

 val imageProvider = imageProvider(Res.drawable.pin_red)

The under-hood multi-platform implementation is very interesting, but it allows us not to think about the use of resources in our map.

Composable as ImageProvider

But the coolest feature is the still experimental function – using composable content as an icon. That is, you can modify the content displayed on the map in runtime using the familiar compose ui.

The official SDK for Android and iOS has the ability to set a view as an icon. However, under the hood, the conversion to ImageProvider it's just rendering directly into the image and saving the image itself. This imposes restrictions that require finding ways to bypass. Let's consider creating such 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)
      }
    }
}

Now let's figure out what's going on here.

  • clicksCount – click counter, changeable by clicking on Placemark, clickable not in this code, because I want to remind you that this content will not be drawn as part of the interface, it will be rendered immediately in Bitmap

  • contentSize – ignored on Android. Only applicable on iOS, since rendering there is done via a very hacky method. Describes the size of the image converted to Bitmap. This method will be removed in future versions of the library, for now it is used as is.

  • imageProvider(size = contentSize, clicksCount) { /* */ }. size has already been discussed above, but here clicksCount passed here to cause a rerender in the bitmap when this field changes

While the library is in active development, the functionality is available only in this form, but you can always contribute to this library to improve it.

Events

To process clicks on objects, nothing special is needed, just pass the callback to our Placemark The Boolean we return is an indicator that we processed this tap.

Placemark(
    icon = clicksImageProvider,
    state = rememberPlacemarkState(placemarkGeometry),
    onTap = {
        clicksCount++
        true
    }
)

Access to the original copy of Map

IN @YandexMapComposable context you can access what was created in YandexMap instance Map . It can be used in specific cases where the States API does not provide the desired functionality.

@Composable
fun MapScreen() {
    YandexMap(/* */) {
        MapEffect { map: Map ->
            // use map here
        }
    }
}

Full access to Map. Controller API

If you plan to interact with the map more extensively, you should choose this method. The idea is to have full access to the map instance and control it only through the nega.

@Composable
fun MapScreen() {
    //...
    val mapController = rememberYandexMapController()
    MapControllerEffect(mapController) { mapWindow ->
        mapWindow.map.move(startPosition)
        mapWindow.map.isZoomGesturesEnabled = true
    }
    YandexMap(
        controller = mapController,
        modifier = Modifier.fillMaxSize(),
    )
}

We don't use the compose runtime and can use the map more efficiently. YandexMap simply creates a map, draws it, and passes the created map instance to YandexMapController.

YandexMapController.mapWindow is null until it is created by YandexMap. To guarantee non-nullity, it is used MapControllerEffect.

Conclusion

Library allows you to simplify the process of integrating Yandex MapKit SDK into a Compose project, including a multi-platform one. At the moment, the library is under active development and some of the functionality is not yet available (although you can perform toNative()if you are writing for one platform, you can read more in the first part).

This is not the end of the article series, there will be several more articles about under the hood of this library. In the meantime, you can read the first part to understand how the wrapper in this library is arranged.

I am not affiliated with Yandex in any way. I am only the author of the library, allowing to use their development, MapKit SDK, in the “ecosystem” of KMP projects. All api keys, necessary for work with SDK are obtained as with the official library, on the Yandex website. I do not claim either your api keys, or money from the purchase of tariffs to Yandex. It is even possible that this library will attract a certain number of clients to Yandex interested in development for KMP. Contacts for communication: Vladimir @vollllodya

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *