Mixpanel en Kotlin Multiplatform sin SDK KMP oficial

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

AspectoAndroidiOS
SDK produccióncom.mixpanel.android (Maven)Mixpanel-swift (CocoaPod)
Helper debugLogcatAnalyticsHelperandroid.util.LogLogAnalyticsHelperIosNSLog (platform.Foundation)
Detección de modoBuildConfig.DEBUGPlatform.isDebugBinary
Tipo de propiedadesJSONObjectMap<Any?, Any?>
Integración en GradleandroidMain.dependenciescocoapods { pod(...) }
Acceso al SDK en KotlinImport directococoapods.Mixpanel_swift.Mixpanel
InicializaciónMixpanelAPI.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.

Alberto Rivas

© 2026 albrivas

LinkedIn GitHub