Kotlin Multiplatform¶
Herald for Kotlin Multiplatform lets you write your events once, in shared Kotlin, and track them from shared view models. It wraps the two native SDKs, so each platform keeps its own setup and vendors:
- on Android, the Android SDK: events from shared code are Android Herald events;
- on iOS, the iOS SDK, through its
HeraldInteropbridge.
| herald-kmp | Android SDK | iOS SDK |
|---|---|---|
| 1.1.0 | 1.4.0 | 1.3.0 |
| 1.0.0 | 1.2.0 | 1.1.0 |
Use it when your app shares Kotlin code between Android and iOS. A native app keeps using its own SDK.
Installation¶
In your shared module, add herald-kmp-core, and export it so Swift can use it:
kotlin {
listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->
target.binaries.framework {
baseName = "Shared"
export("io.github.mkhytarmkhoian:herald-kmp-core:1.1.0")
}
}
sourceSets {
commonMain.dependencies {
api("io.github.mkhytarmkhoian:herald-kmp-core:1.1.0")
}
}
}
| Module | What it is |
|---|---|
herald-kmp-core |
Events, properties and the five capabilities, for shared code. |
herald-kmp-amplitude |
Amplitude's revenue event. |
herald-kmp-adjust |
Adjust's revenue and ad revenue events. |
herald-kmp-appsflyer |
AppsFlyer's purchase, subscription and ad revenue events. |
herald-kmp-meta |
Meta's purchase event. |
Add a vendor's module only for its revenue events, and export it too. Every other event works with the core alone.
Shared code¶
Events and view models are the same Kotlin as on Android:
package io.github.mkhytarmkhoian.herald.sample
import io.github.mkhytarmkhoian.herald.kmp.AnalyticsValue
import io.github.mkhytarmkhoian.herald.kmp.Event
import io.github.mkhytarmkhoian.herald.kmp.EventTrackerService
import io.github.mkhytarmkhoian.herald.kmp.ScreenViewEvent
import io.github.mkhytarmkhoian.herald.kmp.parameters
// Written once, in shared code. On Android these are Android Herald events; on iOS they reach
// herald-ios through the bridge.
public object CheckoutViewed : ScreenViewEvent {
override val name: String = "checkout"
}
public class CheckoutStarted(plan: String, seats: Int, price: Double) : Event {
override val name: String = "checkout_started"
override val parameters: Map<String, AnalyticsValue> = parameters {
put("plan", plan)
put("seats", seats)
put("price", price)
}
}
public class CheckoutViewModel(private val analytics: EventTrackerService) {
public suspend fun opened(): Unit = analytics.track(CheckoutViewed)
public suspend fun started(plan: String, seats: Int): Unit =
analytics.track(CheckoutStarted(plan, seats, price = seats * 4.99))
}
Android¶
Nothing extra. Set Herald up as in any Android app, and give it to your shared code:
val herald = Herald {
provider("amplitude", events = amplitudeTracker /* … */)
}
CheckoutViewModel(analytics = herald)
iOS¶
- In Xcode, add
herald-ioswith itsHeraldCoreandHeraldInteropmodules, and your vendors' packages, as for any iOS app. - Copy two files into your app, once. They can't ship in a package, because they import your app's own shared framework.
- Set herald-ios up as in any iOS app, then connect it to your shared code:
let herald = Herald(providers: [amplitudeProvider(amplitude)])
let bridge = HeraldBridge(herald, translate: revenueEvent, errorReporter: report)
let viewModel = CheckoutViewModel(analytics: IosAnalytics(bridge: HeraldKmpBridge(bridge)))
HeraldKmpBridge.swift: copy it as is
import HeraldInterop
import Shared
/// Connects herald-kmp's shared code to herald-ios. Copy this file into your iOS app as is: it
/// can't ship in a package, because it imports your app's own shared framework.
final class HeraldKmpBridge: NSObject, IosHeraldBridge {
private let bridge: HeraldBridge
init(_ bridge: HeraldBridge) {
self.bridge = bridge
}
func track(
name: String, parameters: [String: IosBridgeValue], isScreenView: Bool,
needsTranslation: Bool, event: any KotlinEvent
) {
bridge.track(
name: name, parameters: parameters.mapValues(Self.value), isScreenView: isScreenView,
needsTranslation: needsTranslation, kotlinEvent: event)
}
func set(name: String, value: IosBridgeValue, isUserProperty: Bool) {
bridge.set(name: name, value: Self.value(value), isUserProperty: isUserProperty)
}
func identify(userId: String) { bridge.identify(userId: userId) }
func reset() { bridge.reset() }
func start() { bridge.start() }
func flush() { bridge.flush() }
func setEnabled(enabled: Bool) { bridge.setEnabled(enabled) }
private static func value(_ value: IosBridgeValue) -> BridgeValue {
switch value.type {
case .string: return .string(value.string)
case .int_: return .int(Int(value.int_))
case .double_: return .double(value.double_)
default: return .bool(value.bool_)
}
}
}
RevenueTranslation.swift: keep the blocks for your vendors
import AppsFlyerLib
import HeraldAdjust
import HeraldAmplitude
import HeraldAppsFlyer
import HeraldCore
import HeraldMeta
import Shared
/// Turns herald-kmp's revenue events into herald-ios's, so each vendor recognises them. Copy this
/// file into your iOS app and keep the blocks for the vendors you use.
@Sendable
func revenueEvent(from kotlinEvent: AnyObject, parameters: [String: AnalyticsValue])
-> (any Event)?
{
if let event = kotlinEvent as? KotlinAmplitudeRevenueEvent {
return AmplitudeRevenueEvent(
name: event.name, price: event.price, quantity: Int(event.quantity),
productId: event.productId, revenueType: event.revenueType, currency: event.currency,
revenue: event.revenue?.doubleValue, receipt: event.receipt,
receiptSig: event.receiptSig, insertId: event.insertId, parameters: parameters)
}
if let event = kotlinEvent as? KotlinAdjustRevenueEvent {
return AdjustRevenueEvent(
name: event.name, revenue: event.revenue, currency: event.currency,
deduplicationId: event.deduplicationId, parameters: parameters)
}
if let event = kotlinEvent as? KotlinAdjustAdRevenueEvent {
return AdjustAdRevenueEvent(
name: event.name, source: event.source, revenue: event.revenue,
currency: event.currency, adImpressionsCount: event.adImpressionsCount?.intValue,
adRevenueNetwork: event.adRevenueNetwork, adRevenueUnit: event.adRevenueUnit,
adRevenuePlacement: event.adRevenuePlacement, parameters: parameters)
}
if let event = kotlinEvent as? KotlinAppsFlyerPurchaseEvent {
return AppsFlyerPurchaseEvent(
name: event.name, revenue: event.revenue, currency: event.currency,
contentId: event.contentId, contentType: event.contentType,
quantity: event.quantity?.intValue, orderId: event.orderId, parameters: parameters)
}
if let event = kotlinEvent as? KotlinAppsFlyerSubscribeEvent {
return AppsFlyerSubscribeEvent(
name: event.name, revenue: event.revenue, currency: event.currency,
parameters: parameters)
}
if let event = kotlinEvent as? KotlinAppsFlyerAdRevenueEvent,
let network = mediationNetworks[event.mediationNetwork.name]
{
return AppsFlyerAdRevenueEvent(
name: event.name, monetizationNetwork: event.monetizationNetwork,
mediationNetwork: network, revenue: event.revenue, currency: event.currency,
parameters: parameters)
}
if let event = kotlinEvent as? KotlinMetaPurchaseEvent {
return MetaPurchaseEvent(
name: event.name, amount: event.amount, currency: event.currency,
parameters: parameters)
}
return nil
}
/// AppsFlyer's mediation networks, by their name in Kotlin. The two SDKs list the same ones.
private let mediationNetworks: [String: MediationNetworkType] = [
"IRONSOURCE": .ironSource, "APPLOVIN_MAX": .applovinMax, "GOOGLE_ADMOB": .googleAdMob,
"FYBER": .fyber, "APPODEAL": .appodeal, "ADMOST": .admost, "TOPON": .topon,
"TRADPLUS": .tradplus, "YANDEX": .yandex, "CHARTBOOST": .chartBoost, "UNITY": .unity,
"TOPON_PTE": .toponPte, "CUSTOM_MEDIATION": .custom,
"DIRECT_MONETIZATION_NETWORK": .directMonetization,
]
If RevenueTranslation.swift misses a revenue event, the bridge reports it to errorReporter
instead of letting the revenue disappear without a word.
Where shared Kotlin differs from Android, and how labels and errors work across the two apps, is in the Kotlin Multiplatform notes.