Skip to content

Kotlin Multiplatform notes

Shared code works the same as on Android. These are the places where it differs.

Values and revenue events

Two things differ from Android, because of Kotlin limits:

  • A property's value is AnalyticsValues.string("dark"), not AnalyticsValue.String("dark").
  • A revenue event is made by the function named after its class, with a small first letter. It has the same parameters and defaults as the Android constructor:
package io.github.mkhytarmkhoian.herald.sample

import io.github.mkhytarmkhoian.herald.kmp.EventTrackerService
import io.github.mkhytarmkhoian.herald.kmp.adjust.adjustAdRevenueEvent
import io.github.mkhytarmkhoian.herald.kmp.adjust.adjustRevenueEvent
import io.github.mkhytarmkhoian.herald.kmp.amplitude.amplitudeRevenueEvent
import io.github.mkhytarmkhoian.herald.kmp.appsflyer.MediationNetwork
import io.github.mkhytarmkhoian.herald.kmp.appsflyer.appsFlyerAdRevenueEvent
import io.github.mkhytarmkhoian.herald.kmp.appsflyer.appsFlyerPurchaseEvent
import io.github.mkhytarmkhoian.herald.kmp.appsflyer.appsFlyerSubscribeEvent
import io.github.mkhytarmkhoian.herald.kmp.meta.metaPurchaseEvent
import io.github.mkhytarmkhoian.herald.kmp.parameters

// Written once. Each vendor gets its revenue on both platforms.
public class StoreViewModel(private val analytics: EventTrackerService) {
    public suspend fun bought(plan: String) {
        val source = parameters { put("source", "paywall") }
        analytics.track(
            amplitudeRevenueEvent(
                name = "purchase", price = 4.99, productId = plan, currency = "USD",
                parameters = source,
            ),
        )
        analytics.track(
            adjustRevenueEvent(
                name = "purchase", revenue = 4.99, currency = "USD", deduplicationId = "order-1",
            ),
        )
        analytics.track(
            appsFlyerPurchaseEvent(
                name = "af_purchase", revenue = 4.99, currency = "USD", contentId = plan,
                quantity = 1,
            ),
        )
        analytics.track(
            appsFlyerSubscribeEvent(name = "af_subscribe", revenue = 4.99, currency = "USD"),
        )
        analytics.track(metaPurchaseEvent(name = "purchase", amount = 4.99, currency = "USD"))
    }

    public suspend fun adShown() {
        analytics.track(
            adjustAdRevenueEvent(
                name = "ad_revenue", source = "applovin_max_sdk", revenue = 0.01, currency = "USD",
                adRevenueNetwork = "AppLovin",
            ),
        )
        analytics.track(
            appsFlyerAdRevenueEvent(
                name = "ad_revenue", monetizationNetwork = "AppLovin",
                mediationNetwork = MediationNetwork.APPLOVIN_MAX, revenue = 0.01, currency = "USD",
            ),
        )
    }
}

Names in Swift

herald-ios has types with the same names as herald-kmp's, so Swift sees the Kotlin ones with "Kotlin" in front: KotlinEvent, KotlinAmplitudeRevenueEvent and so on.

Labels

A label is an interface your events implement. Checks in shared Kotlin and on Android work as usual:

package io.github.mkhytarmkhoian.herald.sample

import io.github.mkhytarmkhoian.herald.kmp.AnalyticsValue
import io.github.mkhytarmkhoian.herald.kmp.AnalyticsValues
import io.github.mkhytarmkhoian.herald.kmp.Event
import io.github.mkhytarmkhoian.herald.kmp.EventTrackerService
import io.github.mkhytarmkhoian.herald.kmp.Property
import io.github.mkhytarmkhoian.herald.kmp.PropertyTrackerService
import io.github.mkhytarmkhoian.herald.kmp.parameters

/** A label: events about a person. Some vendors must never get them. */
public interface PersonalDataEvent : Event

public class ProfileOpened(userId: String) : PersonalDataEvent {
    override val name: String = "profile_opened"
    override val parameters: Map<String, AnalyticsValue> = parameters { put("user_id", userId) }
}

public class AppTheme(dark: Boolean) : Property {
    override val name: String = "app_theme"
    override val value: AnalyticsValue = AnalyticsValues.string(if (dark) "dark" else "light")
}

public class ProfileViewModel(
    private val analytics: EventTrackerService,
    private val properties: PropertyTrackerService,
) {
    public suspend fun opened(userId: String): Unit = analytics.track(ProfileOpened(userId))

    public suspend fun themeChanged(dark: Boolean): Unit = properties.set(AppTheme(dark))
}

A factory written in Swift checks the Kotlin event:

import HeraldCore
import HeraldLog
import Shared

/// An iOS rule, written in Swift: personal data never reaches this log. The label is the shared
/// Kotlin one, checked by type.
struct NoPersonalDataLogFactory: LogEventTrackerFactory {
    func create(_ event: any Event) throws -> Resolution<any LogEventTracker> {
        if event.kotlinEvent is PersonalDataEvent {
            return .dropped
        }
        return .declined
    }
}

Errors

Vendor failures go to the error reporter each app sets up: in Kotlin on Android, in Swift on iOS, on Herald and on HeraldBridge. To report them through shared Kotlin code, call it from Swift's reporter:

let report: AnalyticsErrorReporter = { failure in
    AnalyticsFailuresKt.reportAnalyticsFailure(message: "\(failure)")
}