Al migrar core:analytics a Kotlin Multiplatform, el primer obstáculo fue que Mixpanel no publica un SDK KMP. Tiene SDKs separados para Android y para iOS, incompatibles entre sí. La solución no es buscar una librería wrapper — es diseñar la abstracción correcta en commonMain y dejar que cada plataforma implemente el contrato con su SDK nativo.
La abstracción en commonMain
La interfaz vive íntegramente en commonMain. No hay ninguna referencia a plataformas, ningún expect/actual, ningún tipo de Android o de iOS:
// commonMain
interface AnalyticsHelper {
fun logEvent(event: AnalyticsEvent)
fun updateSuperProperties(properties: Map<String, Any>)
}
data class AnalyticsEvent(
val type: String,
val extras: List<Param> = emptyList(),
) {
data class Param(val key: String, val value: String)
// ...
}
NoOpAnalyticsHelper también vive en commonMain — una implementación vacía que se usa en tests y previews sin side effects.
El principio es simple: todo lo que no depende de una plataforma va en commonMain. La interfaz, el modelo de evento, las constantes de tipos y parámetros. Las implementaciones de Mixpanel son un detalle de plataforma.
Implementación Android
En androidMain, la implementación inyecta Context y MixpanelAPI. El SDK de Android trabaja con JSONObject:
// androidMain
class MixpanelAnalyticsHelper(
private val context: Context,
private val mixpanel: MixpanelAPI,
) : AnalyticsHelper {
init {
val appVersion = runCatching {
context.packageManager.getPackageInfo(context.packageName, 0).versionName.orEmpty()
}.getOrDefault("")
mixpanel.registerSuperPropertiesOnce(
JSONObject().apply {
put("app_version", appVersion)
put("platform", "android")
}
)
}
override fun logEvent(event: AnalyticsEvent) {
val properties = JSONObject()
properties.put(AnalyticsEvent.ParamKeys.CATEGORY, event.category)
event.extras.forEach { param -> properties.put(param.key, param.value) }
mixpanel.track(event.type, properties)
}
override fun updateSuperProperties(properties: Map<String, Any>) {
val jsonProperties = JSONObject()
properties.forEach { (key, value) -> jsonProperties.put(key, value) }
mixpanel.registerSuperProperties(jsonProperties)
}
}
La dependencia del SDK de Android va exclusivamente en androidMain.dependencies:
// build.gradle.kts
sourceSets {
androidMain.dependencies {
implementation(libs.mixpanel)
}
}
Implementación iOS con CocoaPods
Para iOS, el SDK de Mixpanel se integra vía CocoaPods. El módulo necesita el plugin kotlin("native.cocoapods") y declarar el pod:
// build.gradle.kts
plugins {
alias(libs.plugins.gasguru.kmp.library)
kotlin("native.cocoapods")
}
kotlin {
cocoapods {
summary = "GasGuru Analytics — Mixpanel integration for iOS"
homepage = "https://github.com/gasguru/GasGuru"
version = "1.0"
ios.deploymentTarget = "15.0"
pod("Mixpanel-swift") {
version = "~> 4.2"
}
}
}
En iosMain, el SDK se accede mediante el binding generado automáticamente por Kotlin/Native al procesar el pod. El import es cocoapods.Mixpanel_swift.Mixpanel — el nombre del pod con guiones convertidos a guiones bajos:
// iosMain
import cocoapods.Mixpanel_swift.Mixpanel
class MixpanelAnalyticsHelperIos : AnalyticsHelper {
override fun logEvent(event: AnalyticsEvent) {
val properties = mutableMapOf<Any?, Any?>(
AnalyticsEvent.ParamKeys.CATEGORY to event.category,
)
event.extras.forEach { param -> properties[param.key] = param.value }
Mixpanel.mainInstance()?.track(event = event.type, properties = properties)
}
override fun updateSuperProperties(properties: Map<String, Any>) {
val iosProperties = properties.entries
.associate<Map.Entry<String, Any>, Any?, Any?> { (key, value) -> key to value }
.toMutableMap()
Mixpanel.mainInstance()?.registerSuperProperties(properties = iosProperties)
}
}
⚠️ El SDK de iOS espera Map<Any?, Any?> con nulabilidad explícita — no Map<String, Any>. Es necesario convertir el mapa del contrato de la interfaz al tipo que admite el binding de Kotlin/Native.
Inyección de dependencias con Koin
Cada plataforma registra su propia implementación en su módulo Koin, y ambas aplican la misma lógica debug/producción: en debug se usa un helper que loguea en consola sin llamar a Mixpanel, y en producción se usa la implementación real.
En Android, la detección se hace con BuildConfig.DEBUG y el helper de debug usa android.util.Log:
// androidMain — AnalyticsModule.kt
val analyticsModule = module {
single<MixpanelAPI> {
MixpanelAPI.getInstance(androidContext(), null, true)
}
single<AnalyticsHelper> {
if (BuildConfig.DEBUG) LogcatAnalyticsHelper()
else MixpanelAnalyticsHelper(context = androidContext(), mixpanel = get())
}
}
En iOS, el equivalente a BuildConfig.DEBUG en Kotlin/Native es Platform.isDebugBinary. El helper de debug usa NSLog de platform.Foundation:
// iosMain — LogAnalyticsHelperIos.kt
class LogAnalyticsHelperIos : AnalyticsHelper {
override fun logEvent(event: AnalyticsEvent) {
val paramsString = event.extras.joinToString(separator = ", ") { "${it.key}=${it.value}" }
NSLog("▶ [${event.category}] ${event.type} | $paramsString")
}
override fun updateSuperProperties(properties: Map<String, Any>) {
val propsString = properties.entries.joinToString(separator = ", ") { "${it.key}=${it.value}" }
NSLog("⚙ [super_properties] $propsString")
}
}
// iosMain — AnalyticsModuleIos.kt
val analyticsModuleIos = module {
single<AnalyticsHelper> {
if (Platform.isDebugBinary) {
LogAnalyticsHelperIos()
} else {
MixpanelAnalyticsHelperIos()
}
}
}
El comportamiento es idéntico en ambas plataformas: debug loguea en consola, producción envía a Mixpanel. Solo cambia la API de logging (Log.d vs NSLog) y la forma de detectar el modo (BuildConfig.DEBUG vs Platform.isDebugBinary).
La inicialización de Mixpanel en iOS (Mixpanel.initialize(token:)) ocurre en la capa de aplicación antes de que Koin arranque — MixpanelAnalyticsHelperIos accede al singleton ya inicializado con Mixpanel.mainInstance().
Resumen
| Aspecto | Android | iOS |
|---|---|---|
| SDK producción | com.mixpanel.android (Maven) | Mixpanel-swift (CocoaPod) |
| Helper debug | LogcatAnalyticsHelper → android.util.Log | LogAnalyticsHelperIos → NSLog (platform.Foundation) |
| Detección de modo | BuildConfig.DEBUG | Platform.isDebugBinary |
| Tipo de propiedades | JSONObject | Map<Any?, Any?> |
| Integración en Gradle | androidMain.dependencies | cocoapods { pod(...) } |
| Acceso al SDK en Kotlin | Import directo | cocoapods.Mixpanel_swift.Mixpanel |
| Inicialización | MixpanelAPI.getInstance(...) | Mixpanel.mainInstance() |
El patrón es replicable para cualquier SDK de terceros que no tenga soporte KMP oficial: la interfaz en commonMain, cada plataforma con su implementación nativa, y Koin como capa que decide qué se inyecta en cada target.