Integración con Android

Esta guía describe el proceso detallado de integración del SDK de EMMA en una aplicación Android utilizando Kotlin y Android Studio en un entorno de desarrollo basado en Gradle sobre Windows 10

Requisitos previos

ComponenteRequerimiento
Sistema OperativoWindows 10 o superior
IDEAndroid Studio (última versión recomendada 2024.3.1)
KotlinVersión: 2.0.21
GradleVersión: 8.11.1
SDK EMMAVersión: 4.15.5

Pasos para la Integración del SDK de EMMA

  • Incluir el Repositorio en tu Proyecto:

    Luego de crear el proyecto en tu IDE, Abre el archivo settings.gradle.kts (nivel del proyecto) ubicado en el directorio Gradle Scripts y agrega la URL del repositorio de EMMA EMMA( maven { url = uri("https://repo.emma.io/emma") }) en la sección de repositories:

    settings.gradle.kts(:project Settings)
    EMMA( maven { url = uri("https://repo.emma.io/emma") })
    
    Paso 1
  • Añade la dependencia en el archivo build.gradle.kts(:app).

    Escribe esta anotación implementation("io.emma:eMMaSDK:4.15.+") , La versión más reciente del SDK es la 4.15.5. Consulta esta página para detalles sobre las actualizaciones.

    build.gradle.kts(:app)
      implementation("io.emma:eMMaSDK:4.15.5")
    
    Paso 2
  • Obtener el Session Key de EMMA

    Para comenzar a utilizar EMMA, puedes guiarte con este enlace: y así crear una cuenta; Una vez registrada, configura tu cuenta siguiendo los pasos indicados aquí y luego solicita tus credenciales: la EMMA Key y la API Key, necesarias para habilitar la integración, aquí puedes ver la guía.

    Paso 3
  • Permisos requeridos por el SDK

    El SDK contiene por defecto los siguientes permisos obligatorios. Estos permisos NO se tienen que añadir en el AndroidManifest.xml de la aplicación, ya que es el mismo SDK quien los añade:

    Paso 4

    Si quieres habilitar la localización, tienes que añadir los siguientes permisos al AndroidManifest.xml de tu aplicación:

    AndroidManifest.xml
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
    Paso 5
  • Sincroniza tu proyecto

    Al sincronizar Gradle, se actualizan dependencias, se configuran ajustes y se procesan los archivos build.gradle, permitiendo que la app se compile correctamente y sin errores.

    Paso 6
  • Inicializar la librería

    En tu clase Application, añade lo siguiente:

    ExampleApplication.kts
    import android.app.Application
    import io.emma.android.EMMA
    
    class ExampleApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
    
        val configuration = EMMA.Configuration.Builder(this)
            .setSessionKey("TU_SESION_KEY")
            .trackScreenEvents(false)
            .setDebugActive(BuildConfig.DEBUG)
            .build()
    
        EMMA.getInstance().startSession(configuration)
    }
    }
    
    Recuerda habilitar esta función en tu archivo build.gradle.kts(:app) .
    build.gradle.kts(:app)
    buildFeatures {
        buildConfig = true
    }
    
    Paso 8
    Omitir este paso podría generar un error de la clase BuildConfig.
    Paso 7
    Para asegurarte de que tu clase ExampleApplication se ejecute correctamente al iniciar la aplicación, debes registrarla en el archivo AndroidManifest.xml dentro de la etiqueta application.
    Paso 9
  • Verificar en la plataforma de EMMA

    Al configurar el SDK en la app, es importante verificar en la plataforma de EMMA (dashboard) que los usuarios estén siendo registrados como activos. Esto confirma que la integración del SDK fue exitosa y que la app está enviando correctamente los eventos de inicio de sesión e instalación.

    Paso 10

Desactivar envío de pantallas

El envío de pantallas está activo por defecto en el SDK de EMMA.Para desactivarlo usa la siguiente configuración: trackScreenEvents(false).

ExampleApplication.kts
import android.app.Application
import io.emma.android.EMMA

class ExampleApplication : Application() {

override fun onCreate() {
    super.onCreate()

    val configuration = EMMA.Configuration.Builder(this)
        .setSessionKey("TU_SESION_KEY")
        .trackScreenEvents(false) 
        .setDebugActive(BuildConfig.DEBUG)
        .build()

    EMMA.getInstance().startSession(configuration)
}
}

Política de familias

Para todas aquellas apps que están dentro del programa Designed for Families necesitan cumplir una serie de requisitos respecto a la información a compartir. Para ello EMMA ha habilitado una propiedad en el arranque de sesión para asegurar el cumplimiento de esta política.

ExampleApplication.kts
import android.app.Application
import io.emma.android.EMMA

class ExampleApplication : Application() {

override fun onCreate() {
    super.onCreate()

    val configuration = EMMA.Configuration.Builder(this)
        .setSessionKey("TU_SESION_KEY")
        .setFamiliesPolicyTreatment(true) 
        .setDebugActive(BuildConfig.DEBUG)
        .build()

    EMMA.getInstance().startSession(configuration)
}
}

Además, es importante deshabilitar el permiso para recolectar el Google Advertasing ID en el AndroidManifest.

AndroidManifest.xml
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove"/>

Proguard

Si utilizas Proguard o alguna alternativa compatible, a continuación tenemos un ejemplo del contenido del fichero de reglas proguard-rules.pro. Puede que tengas que modificar otras reglas dependiendo de cada aplicación.

proguard-rules.pro
# EMMA SDK
-keep class io.emma.android.** { *; }

# Rules for play services ads identifier
-keep class com.google.android.gms.common.ConnectionResult {
int SUCCESS;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
java.lang.String getId();
boolean isLimitAdTrackingEnabled();
}

# Rule for google play referrer
-keep public class com.android.installreferrer.** { *; }

# Keep generic signatures; needed for correct type resolution
-keepattributes Signature

# Keep Gson annotations
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-if class com.google.gson.reflect.TypeToken
-keep,allowobfuscation class com.google.gson.reflect.TypeToken
-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.Expose <fields>;
@com.google.gson.annotations.JsonAdapter <fields>;
@com.google.gson.annotations.Since <fields>;
@com.google.gson.annotations.Until <fields>;
}

-keepclassmembers class * extends com.google.gson.TypeAdapter {
<init>();
}
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
<init>();
}
-keepclassmembers class * implements com.google.gson.JsonSerializer {
<init>();
}
-keepclassmembers class * implements com.google.gson.JsonDeserializer {
<init>();
}

-if class *
-keepclasseswithmembers,allowobfuscation class <1> {
@com.google.gson.annotations.SerializedName <fields>;
}
-if class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keepclassmembers,allowobfuscation,allowoptimization class <1> {
<init>();
}

# Rules for retrofit2
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
-keepattributes AnnotationDefault
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn javax.annotation.**
-dontwarn kotlin.Unit
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>

-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface * extends <1>

-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

-keep,allowobfuscation,allowshrinking class retrofit2.Response

# Rules for okhttp3
-keepattributes Signature
-keepattributes Annotation
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**

# Rules for glide (used to display images by sdk)
-keep class com.bumptech.glide.** { *; }
-dontwarn com.bumptech.glide.**

# Rules for push
-keep class com.google.firebase.** { *; }

# Rules for Huawei hms push and odid identifier
-ignorewarnings
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-keep class com.huawei.hianalytics.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}

# Rules for Huawei ads-identifier and ads referrer
-keep class com.huawei.hms.ads.** { *; }
-keep interface com.huawei.hms.ads.** { *; }
Asegúrate de tener habilitada la minificación y configurado el archivo de reglas
build.gradle.kts(app)
android {
buildTypes {
    release {
        isMinifyEnabled = true
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}
}

Luego utiliza este comando en tu terminal ./gradlew assembleRelease para que Proguard compile y ofusque el código con las reglas descritas en el paso anterior

Bash
./gradlew assembleRelease

Verifica que haya compilado correctamente, debe aparecer un mensaje similar a esto:

Proguard
Desde Android Gradle Plugin (AGP) 3.4+, R8 reemplazó a ProGuard por defecto.

Integración Notificaciones Push

EMMA ofrece un completo sistema de envío y reporting de Notificaciones Push fácil de integrar usando Firebase Cloud Messaging (FCM) en Android.

  • Obtener Sender ID, Server Key y generar certificado SHA

    Obtén en primer lugar tu propio Sender ID y Server Key para FCM como se específica en este artículo.

    Debes considerar al configurar el proyecto en Firebase generar las Huellas digitales del certificado SHA con el siguiente comando ./gradlew signingReport dentro de tu proyecto de Kotlin, usando la terminal.

    Bash
        ./gradlew signingReport
    
    SHA
  • Generar clave privada JSON

    Genera un archivo JSON de clave privada entrando en la configuración de tu proyecto de Firebase , Cuentas de servicio, y generar nueva clave privada.

    Clave privada
  • Configurar la plataforma EMMA

    Además debes configurar la plataforma EMMA con ese archivo JSON de la siguiente manera; Entra al menu del borde superior y accede a preferencias app, has click en Selecciona un fichero JSON.

    Clave privada json
  • Integrar FCM en tu Service

    Hay que añadir el siguiente service al AndroidManifest.xml:

    AndroidManifest.xml
    <service
    android:name="io.emma.android.push.EMMAFcmMessagingService"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
    <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
    </service>
    
    Service push
  • Configurar dependencias de Google Services nivel proyect

    Agrega el complemento de Google Services como dependencia en el archivo de Gradle build.gradle.kts a nivel de proyecto: Aquí más información sobre integración de Firebase

    settings.gradle.kts(:project)
    
    id("com.google.gms.google-services") version "4.3.10" apply false
    
    Firebase
  • Configurar dependencias de Google Services nivel app

    Agrega el complemento de los servicios de Google en el archivo de Gradle build.gradle.kts a nivel de app.

    settings.gradle.kts(:app)
    
    id("com.google.gms.google-services")
    

    Agrega los SDK de Firebase a tu app en el archivo Gradle build.gradle.kts a nivel de app, agrega las dependencias de los productos de Firebase que quieras usar en tu app. Es recomendable que uses la Firebase Android BoM para controlar las versiones de las bibliotecas.

    settings.gradle.kts(:app)
    implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
    
    Firebase App
  • Iniciando el sistema de push

    Inicia el sistema de push debajo del inicio de sesión en Application, esta anotación usa un icono por defecto se puede personalizar. Debes anotar el siguiente código:

    package com.example.integrationkotlin
    import android.app.Application
    import io.emma.android.EMMA
    
    class ExampleApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
    
        val configuration = EMMA.Configuration.Builder(this)
            .setSessionKey("TU_SESION_KEY")
            .trackScreenEvents(false)
            .setDebugActive(BuildConfig.DEBUG)
            .build()
        
        val pushOpt =          EMMAPushOptions.Builder(MainActivity::class.java,android.R.drawable.ic_dialog_info)
            .setNotificationColor(android.graphics.Color.BLUE)
            .setNotificationChannelName("Notificaciones")
            .build()
    
        EMMA.getInstance().startSession(configuration)
        EMMA.getInstance().startPushSystem(pushOpt)
    }
    }
    

    Desde Android 13, para recibir notificaciones es necesario solicitar un permiso al usuario. Para ello, EMMA ha añadido un método al SDK disponible en la versión 4.12 o superiores. (En mi caso uso API 29), por lo tanto, no lo implementaré, este método tiene que ser llamado en un Activity.

  • Añadir el método onNewIntent()

    Añadir el método onNewIntent() llamando a EMMA.onNewNotification(), que verificará si el usuario ha recibido una notificación cuando la app está abierta.

    Kotlin
    override fun onNewIntent(intent: Intent) {
      super.onNewIntent(intent);
      EMMA.getInstance().onNewNotification(intent,false);
    }
    
    Modifica tu archivo y añade las líneas anteriores, en la documentación oficial la anotación this hace referencia al contexto , en este caso particular la apps ya añade contexto y se quita esa anotación además no añado una url por lo tanto en vez de true que verifica esto quedó en false.

Integración Behavior

Con EMMA puedes conocer la localización de tus usuarios, cómo se registran en tu App, cuántas transacciones realizan y hasta sus características propias. Es decir, toda la información de tus usuarios que obtendrás en la sección de Behavior.

Medición de eventos

La plataforma de EMMA hace la diferenciación entre dos tipos de eventos: los que la plataforma incluye por defecto y los eventos personalizados (custom) que puedes integrar según la estructura de tu aplicación.

Eventos por defecto

Puedes consultar más información sobre los eventos por defecto aquí

Es importante destacar que, aunque la integración esté funcionando correctamente, se debe prestar atención al tipo de dato que se maneja en cada uno de los métodos, especialmente en valores como Double, String, o estructuras como MapString, ya que un tipo incorrecto puede provocar errores en tiempo de ejecución o datos mal registrados en la plataforma de EMMA.

  • Medir Registro

    El método EMMA.getInstance().registerUser() permite enviar información sobre los registros en la aplicación.

    Kotlin
    fun register() {
        EMMA.getInstance().registerUser("554234", "test@emma.io")
    }
    
  • Medir transacciones

    EMMA permite medir cualquier transacción o compra que se realice en tu app. Este es el ejemplo para medir una transacción:

    Kotlin
    fun trackTransaction() {
        EMMA.getInstance().startOrder("<ORDER_ID>", "<CUSTOMER_ID>", 10.0, "")
        EMMA.getInstance().addProduct("<PRODUCT_ID>", "<PRODUCT_NAME>", 1.0, 10.0)
        EMMA.getInstance().trackOrder()
    }
    
  • Iniciar transacción

    El método para iniciar la transacción es EMMA.getInstance().startOrder().

    Kotlin
    EMMA.getInstance().startOrder("<ORDER_ID>", "<CUSTOMER_ID>", 10.0, "")
    
  • Añadir Productos a la transacción

    Una vez iniciada la transacción, hay que añadir los productos a la misma. Para ello usaremos el método EMMA.getInstance().addProduct().

    Kotlin
    EMMA.getInstance().addProduct("<PRODUCT_ID>", "<PRODUCT_NAME>", 1.0, 10.0)
    
  • Medición de la transacción

    Una vez tenemos todos los productos añadidos, ejecutamos la medición de la transacción con el método EMMA.getInstance().trackOrder().

    Kotlin
    EMMA.getInstance().trackOrder()
    
  • Cancelar una transacción

    En el caso de que se necesite cancelar el tracking de una transacción, usaremos el método EMMA.getInstance().cancelOrder().

    Kotlin
    fun cancelTransaction() {
        EMMA.getInstance().cancelOrder("<ORDER_ID>")
    }
    
  • Eventos personalizados

    Este evento fue implementado siguiendo la arquitectura por capas de la app, lo que permite mantener una separación clara entre la lógica de presentación (UI), el dominio y el acceso al SDK (data).

  • MainActivity.kt

    Este botón dispara el evento personalizado desde la UI.

    img custom
  • UI – BehaviorViewModel.kt

    Encapsula la lógica del evento personalizado para ser reutilizada fácilmente.

    img custom
  • Dominio – BehaviorTracker.kt

    Define el contrato que implementará la capa de datos.

    img custom
  • Data – BehaviorTrackerImpl.kt

    Aquí es donde se construye el EMMAEventRequest, se agregan los atributos y finalmente se envía el evento al SDK.

    img custom
  • La documentación oficial da como ejemplo el siguiente método: Puedes consultar más información sobre los eventos personalizados aquí.

    Kotlin
    val eventRequest = EMMAEventRequest("f983d4bef8fc44dad43a1bb30dde9e3c")
    //Optional: custom attributes
    eventRequest.attributes = attributes
    //Optional: request status listener
    eventRequest.requestListener = requestListener
    //Optional: cumtom id for request delegate
    eventRequest.customId = customId
    
    EMMA.getInstance().trackEvent(eventRequest)
    
  • Identificador de EMMA

    Podemos recuperar el identificador interno de EMMA con el método getUserID():

    Kotlin
    class MainActivity : BaseActivity(), EMMAUserInfoInterface {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            EMMA.getInstance().getUserID()
        }
    
        override fun OnGetUserInfo(userInfo: JSONObject?) {
          // Not implemented
        }
    
        override fun OnGetUserID(id: Int) {
                Log.d("MainActivity", id.toString())
        }
    }
    
  • Identificador de dispositivo

    El formato del identificador es del tipo UUID V4. Para obtener el identificador del dispositivo usa el siguiente método:

    Kotlin
    EMMA.getInstance().getDeviceId()
    
  • Identificador de usuario del cliente (Customer ID)

    Para enviar el customer ID independientemente del login/registro usa el siguiente método:

    Kotlin
    EMMA.getInstance().setCustomerId("<Customer ID>")
    
  • Configuración del idioma del usuario

    Establece manualmente el idioma preferido del usuario. Este método permite sobrescribir el idioma predeterminado del dispositivo para establecer un idioma personalizado que se utilizará en todas las peticiones del SDK. Esto resulta útil en aplicaciones que permiten al usuario seleccionar un idioma diferente al configurado en el dispositivo. Se debe usar el código de idioma en formato ISO 639-1:

    Kotlin
    // En este caso, establece inglés como idioma
    EMMA.getInstance().setUserLanguage("en")
    
    Si no se llama a este método, EMMA utilizará por defecto el idioma preferido del usuario configurado en el sistema del dispositivo.
  • Perfil del usuario (User Info)

    Si necesitamos recuperar el perfil del usuario desde la aplicación usaremos el método getUserInfo():

    Kotlin
    class MainActivity : BaseActivity(), EMMAUserInfoInterface {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            EMMA.getInstance().getUserInfo()
        }
    
        override fun OnGetUserInfo(userInfo: JSONObject?) {
          userInfo?.let {
            // Do something with userInfo
          }
        }
    
        override fun OnGetUserID(id: Int) {
                // Not implemented
        }
    }
    
  • Información de la atribución de la instalación

    Después del proceso de atribución, EMMA pone a disposición del SDK la información de atribución del usuario.

    Para obtener la información de atribución usaremos el método getInstallAttributionInfo:

    Kotlin
    class MainActivity : BaseActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
             EMMA.getInstance().getInstallAttributionInfo { attribution ->
                if (attribution != null) {
                   // Do something with attribution
                }
            }
        }
    }
    

Integración In-App Messaging

EMMA incluye 7 formatos comunicativos diferentes que puedes integrar para impactar a tus usuarios en Android:

  • NativeAd - Formato que integra tus comunicaciones respetando siempre el formato y estilo de los contenidos de la App.

  • StartView - Formato que se despliega en toda la pantalla mostrando contenido web.

  • AdBall - Formato comunicativo con forma de burbuja, cuyo click provoca el despliegue de un contenido web.

  • Banner - Formato básico para tus comunicaciones que puedes utilizar para comunicar mensajes específicos tanto arriba como abajo de la pantalla.

  • Strip - Formato comunicativo que ocupa la barra de notificaciones del dispositivo y que muestra un mensaje de derecha a izquierda.

  • Coupon - Formato que te da la opción de canjear cupones si tienes un lugar físico controlando el número de redenciones, el código promocional y la fecha de activación y finalización.

NativeAd

EMMA NativeAd te permite obtener la información de un NativeAd correspondiente a una plantilla que se haya definido y configurado en la plataforma de EMMA.

  • Data - NativeAdDataSource.kt

    Esta clase implementa la fuente de datos para anuncios nativos. Forma parte de la capa de datos de la arquitectura y encapsula por completo las interacciones con el SDK.Ofrece dos métodos públicos para obtener anuncios; getNativeAd(...) para solicitar un anuncio individual y getNativeAdBatch(...) para solicitar un lote (batch) de anuncios.

    NativeAdDataSource.kt
    class NativeAdDataSource : NativeAdDataSourceInterface,
        EMMAInAppMessageInterface,
        EMMABatchNativeAdInterface,
        EMMANativeAdInterface {
    
        private var singleAdCallback: ((EMMANativeAd) -> Unit)? = null
        private var batchAdCallback: ((List<EMMANativeAd>) -> Unit)? = null
    
        override fun getNativeAd(templateId: String, callback: (EMMANativeAd) -> Unit) {
            singleAdCallback = callback
            val request = EMMANativeAdRequest().apply {
                this.templateId = templateId
            }
            EMMA.getInstance().getInAppMessage(request, this)
        }
    
        override fun getNativeAdBatch(templateId: String, callback: (List<EMMANativeAd>) -> Unit) {
            batchAdCallback = callback
            val request = EMMANativeAdRequest().apply {
                this.templateId = templateId
                this.isBatch = true
            }
            EMMA.getInstance().getInAppMessage(request, this)
        }
    
        override fun onReceived(nativeAd: EMMANativeAd) {
            Log.d("NativeAd", "NativeAd recibido: ${nativeAd.nativeAdContent["Title"]?.fieldValue}")
            EMMA.getInstance().sendInAppImpression(CommunicationTypes.NATIVE_AD, nativeAd)
            singleAdCallback?.invoke(nativeAd)
        }
    
        override fun onBatchReceived(nativeAds: MutableList<EMMANativeAd>) {
            Log.d("NativeAd", "Batch recibido con ${nativeAds.size} anuncios.")
            nativeAds.forEach {
                EMMA.getInstance().sendInAppImpression(CommunicationTypes.NATIVE_AD, it)
            }
            batchAdCallback?.invoke(nativeAds)
        }
    
        override fun onShown(campaign: EMMACampaign?) {
            Log.d("NativeAd", "NativeAd mostrado.")
        }
    
        override fun onHide(campaign: EMMACampaign?) {
            Log.d("NativeAd", "NativeAd ocultado.")
        }
    
        override fun onClose(campaign: EMMACampaign?) {
            Log.d("NativeAd", "NativeAd cerrado.")
        }
    }
    
    Aquí el ejemplo de la documentación oficial
  • Interface – NativeAdRepository.kt

    Crear interface que actúe como puente entre la UI/ViewModel y la capa de datos (NativeAdDataSource)

    NativeAdDataSourceInterface.kt
    interface NativeAdDataSourceInterface {
      fun getNativeAd(templateId: String, callback: (EMMANativeAd) -> Unit)
      fun getNativeAdBatch(templateId: String, callback: (List<EMMANativeAd>) -> Unit)
    }
    
  • Repositorio – NativeAdRepository.kt

    Delega el acceso a los datos al dataSource (que implementa la interfaz).

    NativeAdRepository.kt
    class NativeAdRepository(
      private val dataSource: NativeAdDataSourceInterface
    ) {
      fun getNativeAd(templateId: String, callback: (EMMANativeAd) -> Unit) {
          dataSource.getNativeAd(templateId, callback)
      }
    
      fun getNativeAdBatch(templateId: String, callback: (List<EMMANativeAd>) -> Unit) {
          dataSource.getNativeAdBatch(templateId, callback)
      }
    }
    
  • UI – NativeAdManager.kt

    Esta clase encapsula la lógica de carga, renderizado y tracking de anuncios nativos utilizando el SDK de EMMA. Actúa como intermediario entre la capa de dominio (repositorio) y la interfaz de usuario.

    NativeAdManager.kt
    class NativeAdManager(
    private val context: Context,private val container: RelativeLayout) {
    
      private val repository = NativeAdRepository(NativeAdDataSource())
    
      fun loadNativeAd( templateId: String,onAdLoaded: ((EMMANativeAd) -> Unit)? = null,onNoCampaigns: (() -> Unit)? = null) {  
      }
      fun loadNativeAdBatch(templateId: String,onNoCampaigns: (() -> Unit)? = null) {
      }
      private fun renderNativeAd(nativeAd: EMMANativeAd) {
      }
      private fun renderContent( view: View,title: String,body: String,imageUrl: String?,ctaText: String,nativeAd:EMMANativeAd) {
      }
      private fun openNativeAd(nativeAd: EMMANativeAd) {
      }
      private fun trackImpression(nativeAd: EMMANativeAd) {
      }
      fun trackClick(nativeAd: EMMANativeAd) {
      }
    }
    
    
  • Manejo de anuncio único

    Este método solicita un anuncio nativo individual desde el repositorio. Si no hay campañas activas (contenido vacío), dispara el callback onNoCampaigns.

    NativeAdManager.kt
    fun loadNativeAd(
      templateId: String,
      onAdLoaded: ((EMMANativeAd) -> Unit)? = null,
      onNoCampaigns: (() -> Unit)? = null
    ) {
      repository.getNativeAd(templateId) { nativeAd ->
          if (nativeAd.nativeAdContent.isEmpty()) {
              onNoCampaigns?.invoke()
              return@getNativeAd
          }
    
          renderNativeAd(nativeAd)
          onAdLoaded?.invoke(nativeAd)
      }
    }
    
    
  • Manejo de multiples anuncios

    Permite cargar múltiples anuncios nativos de forma simultánea (modo batch) y los renderiza en el contenedor de la UI.

    NativeAdManager.kt
    fun loadNativeAdBatch(
      templateId: String,
      onNoCampaigns: (() -> Unit)? = null
    ) {
      repository.getNativeAdBatch(templateId) { ads ->
          container.removeAllViews()
          if (ads.isEmpty()) {
              onNoCampaigns?.invoke()
              return@getNativeAdBatch
          }
    
          ads.forEach { ad -> renderNativeAd(ad) }
      }
    }
    
    
  • Renderizado de anuncio

    Este método convierte los datos del anuncio en una vista visible dentro del RelativeLayout. Utiliza Glide para cargar imágenes y asigna valores a los campos de texto.

    NativeAdManager.kt
    private fun renderNativeAd(nativeAd: EMMANativeAd) {
      val view = LayoutInflater.from(context).inflate(R.layout.view_native_ad, container, false)
      val content = nativeAd.nativeAdContent
      val containerField = content["container"]
    
      containerField?.fieldContainer?.forEach { side -> 
          renderContent(view, side["Title"]?.fieldValue ?: "", ...)
      } ?: run {
          renderContent(view, content["Title"]?.fieldValue ?: "", ...)
      }
    
      container.addView(view)
      trackImpression(nativeAd)
    }
    
    
  • Registro de impresión y clics

    Estos métodos notifican al SDK que un anuncio fue visto o clickeado, utilizando los métodos oficiales sendInAppImpression y sendInAppClick.

    NativeAdManager.kt
    private fun trackImpression(nativeAd: EMMANativeAd) {
      EMMA.getInstance().sendInAppImpression(CommunicationTypes.NATIVE_AD, nativeAd)
    }
    
    fun trackClick(nativeAd: EMMANativeAd) {
      EMMA.getInstance().sendInAppClick(CommunicationTypes.NATIVE_AD, nativeAd)
    }
    
    
  • MainActivity – Integración de Native Ads

    Esta actividad conecta con NativeAdManager para gestionar los anuncios nativos.

    MainActivity.kt
    private fun loadNativeAd() {
          val templateId = "native-ad-kotlin"
          val adManager = NativeAdManager(this, nativeAdContainer)
          adManager.loadNativeAd(templateId)
      }
    
      private fun loadNativeAdBatch() {
          val templateId = "native-ad-kotlin"
          val adManager = NativeAdManager(this, nativeAdContainer)
          adManager.loadNativeAdBatch(templateId)
      }
    

StartView

La StartView es un formato de comunicación que te permite mostrar un contenido HTML, alojado en una URL, en un WebView a pantalla completa.

StartView
fun getStartView() {
 val startViewRequest = EMMAInAppRequest(EMMACampaign.Type.STARTVIEW)
 EMMA.getInstance().getInAppMessage(startViewRequest)
}

AdBall

El AdBall es una pequeña vista circular que muestra una imagen. Esta vista se puede arrastrar por toda la pantalla y eliminar de ella en cualquier momento, contiene un CTA que es una URL con contenido HTML que lanza un WebView al hacer clic en la ella.

AdBall
fun getAdBall() {
 val adBallRequest = EMMAInAppRequest(EMMACampaign.Type.ADBALL)
 EMMA.getInstance().getInAppMessage(adBallRequest)
}

Banner

El banner es un formato de comunicación que permite adaptar una imagen o GIF en formato banner dentro de una pantalla de la aplicación. Este banner se puede mostrar en el de la pantalla dónde se muestra o en el botom de esta. El banner contiene un CTA configurable en el Dashboard de EMMA y puede ser un deeplink o una URL https. En el caso de ser la segunda opción, al hacer hacer clic se abre un WebView con el contenido de la URL.

Se recomienda hacer la llamada después de que todos los elementos de la pantalla estén cargados.
Banner
fun getBanner() {
 val bannerRequest = EMMAInAppRequest(EMMACampaign.Type.BANNER)
 EMMA.getInstance().getInAppMessage(bannerRequest);
}

Strip

El strip te permite mostrar un banner en lo alto de la pantalla del dispositivo con un texto que va pasando a modo de carrusel. Variables como el tiempo de duración de la rotación o el tiempo de visualización son configurables desde el Dashboard.

Strip
fun getStrip() {
 val stripRequest = EMMAInAppRequest(EMMACampaign.Type.STRIP)
 EMMA.getInstance().getInAppMessage(stripRequest);
}

Coupon

EMMA Coupons te permite obtener, verificar y canjear cupones que se hayan definido y configurado en la plataforma de EMMA. Para crear un cupón en la plataforma EMMA puedes ver este tutorial.

  • Data - CouponDataSource.kt

    Esta clase implementa la fuente de datos para los Coupones. Forma parte de la capa de datos de la arquitectura y se comunica directamente con el SDK de EMMA ´(EMMA.getInstance())´. Gestiona todas las operaciones relacionadas con cupones; Obtención de cupones, Redención de cupones, Cancelación, Consulta de redenciones válidas

    CouponDataSource.kt
    class CouponDataSource : CouponDataSourceInterface, EMMACouponsInterface {
      private var couponsCallback: ((List<EMMACoupon>) -> Unit)? = null
      private var redemptionCallback: ((Boolean) -> Unit)? = null
      private var cancelCallback: ((Boolean) -> Unit)? = null
      private var redeemCountCallback: ((Int) -> Unit)? = null
    
      override fun getCoupons(callback: (List<EMMACoupon>) -> Unit) {
      }
    
      override fun redeemCoupon(couponId: String, callback: (Boolean) -> Unit) {
      }
    
      override fun cancelCoupon(couponId: String, callback: (Boolean) -> Unit) {
      }
    
      override fun getCouponRedeemCount(couponId: String, callback: (Int) -> Unit) {
      }
    
      override fun onCouponsReceived(coupons: List<EMMACoupon>) {
      }
    
      override fun onCouponsFailure() {
      }
    
      override fun onCouponRedemption(success: Boolean) {
      }
    
      override fun onCouponCancelled(success: Boolean) {
      }
    
      override fun onCouponValidRedeemsReceived(numRedeems: Int) {
      }
    }
    
    
    Aquí el ejemplo de la documentación oficial
  • Interface - CouponDataSourceInterface.kt

    Crear interface que actúe como puente entre la UI/ViewModel y la capa de datos (CouponDataSource)

    CouponDataSourceInterface.kt
    interface CouponDataSourceInterface {
      fun getCoupons(callback: (List<EMMACoupon>) -> Unit)
      fun redeemCoupon(couponId: String, callback: (Boolean) -> Unit)
      fun cancelCoupon(couponId: String, callback: (Boolean) -> Unit)
      fun getCouponRedeemCount(couponId: String, callback: (Int) -> Unit)
    }
    
    
  • Repositorio - CouponRepository.kt

    Delega el acceso a los datos al dataSource (que implementa la interfaz).

    CouponRepository.kt
    class CouponRepository(private val dataSource: CouponDataSourceInterface) {
      fun getCoupons(callback: (List<EMMACoupon>) -> Unit) {
          dataSource.getCoupons(callback)
      }
    
      fun redeemCoupon(couponId: String, callback: (Boolean) -> Unit) {
          dataSource.redeemCoupon(couponId, callback)
      }
    }
    
    
  • UI - CouponManager.kt

    Esta clase transforma los datos del cupón en vistas visibles dentro de un LinearLayout. Utiliza LayoutInflater para inflar la vista desde XML, asigna los textos y configura la acción del botón para redimir.

    CouponManager.kt
    
    class CouponManager(
      private val context: Context,
      private val container: LinearLayout 
    ) {
      private val repository = CouponRepository(CouponDataSource())
      }
    
      private fun renderCoupon(coupon: EMMACoupon) {
      }
    
      private fun redeemCoupon(coupon: EMMACoupon) {   
      }
    
    
  • Carga y renderizado del cúpon

    Cuando se llama a loadCoupons(), se limpian primero todas las vistas dentro del contenedor. Luego, se consulta el repositorio para obtener los cupones, y por cada uno se ejecuta renderCoupon() para mostrarlo visualmente. El método renderCoupon() infla una vista desde el layout XML, coloca los datos del cupón (título y descripción) y configura un botón para redimirlo.

    CouponManager.kt
    fun loadCoupons() {
      container.removeAllViews()
      repository.getCoupons { coupons ->
          coupons.forEach { renderCoupon(it) }
      }
    }
    private fun renderCoupon(coupon: EMMACoupon) {
      val view = LayoutInflater.from(context).inflate(R.layout.view_coupon_item, container, false)
    
      view.findViewById<TextView>(R.id.tv_coupon_title).text = coupon.title
      view.findViewById<TextView>(R.id.tv_coupon_description).text = coupon.description
    
      view.findViewById<Button>(R.id.btn_redeem).setOnClickListener {
          redeemCoupon(coupon)
      }
    
      container.addView(view)
    }
    
  • Redención de cupones

    Cuando un usuario presiona el botón de redención, se llama a redeemCoupon(coupon). Este método envía la solicitud de redención al CouponRepository, y si es exitosa, se recarga la lista de cupones.

    CouponManager.kt
    private fun redeemCoupon(coupon: EMMACoupon) {
      val couponIdString = coupon.couponId.toString()
    
      repository.redeemCoupon(couponIdString) { success ->
          Toast.makeText(
              context,
              if (success) "Cupón canjeado!" else "Error al canjear",
              Toast.LENGTH_SHORT
          ).show()
    
          if (success) {
              container.removeAllViews()
              loadCoupons()
          }
      }
    }
    
  • MainActivity - Integración de Coupon

    Esta actividad conecta con CouponManager para gestionar los anuncios nativos.

    MainActivity.kts
    class MainActivity : AppCompatActivity(), EmmaPluginCallback {
      private lateinit var couponContainer: LinearLayout
      private lateinit var couponManager: CouponManager
    
      private fun setupViews() {
          ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
              val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
              v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
              insets
          }
    
          couponContainer = findViewById(R.id.coupon_container)
          couponManager = CouponManager(this, couponContainer)
          couponManager.loadCoupons()
      }
    }
    

Plugins personalizados

A partir de la versión 4.9.0 se ha añadido la posibilidad de poder añadir plugins in-app al SDK. Los plugins in-app funcionan a través de la tecnología de NativeAd.

  • Data - CustomEmmaPlugin.kts

    Puedes crear tu propio formato de comunicación y convertirlo en un plugin in-app, para ello es necesario que la clase principal del nuevo formato extienda de la clase abstracta EMMAInAppPlugin, esta clase obliga a sobrescribir dos métodos; show() y dismiss().

    CustomEmmaPlugin.kts
    class CustomEmmaPlugin : EMMAInAppPlugin() {
    
      private var callback: EmmaPluginCallback? = null
    
      companion object {
          private const val TAG = "CustomEmmaPlugin"
          private const val FIELD_TITLE = "Title"
          private const val FIELD_BODY = "Body"
          private const val FIELD_CUSTOM = "custom"
      }
    
      fun setPluginCallback(callback: EmmaPluginCallback) {
      }
      override fun getId(): String = "emma-plugin-test-plugin"
    
      override fun show(context: Activity?, nativeAd: EMMANativeAd) {
          context?.let { activity ->
              try {
                  val pluginData = extractPluginData(nativeAd)
                  callback?.onEmmaPluginLoaded(pluginData)
                  sendImpression(nativeAd) // Informa a EMMA que el plugin fue mostrado
                  invokeShownListeners(nativeAd)
              } catch (e: Exception) {
                  val errorMessage = "Error en plugin: ${e.message}"
                  Log.e(TAG, errorMessage)
                  callback?.onEmmaPluginError(errorMessage)
              }
          } ?: run {
              callback?.onEmmaPluginError("Contexto nulo en show()")
          }
      }
    
      private fun extractPluginData(nativeAd: EMMANativeAd): EmmaPluginData {
      }
    
      fun handleClick(pluginData: EmmaPluginData) {
      }
    
      override fun dismiss() {
          callback?.onEmmaPluginClosed()
      }
    }
    
    
    Aquí el ejemplo de la documentación oficial y puedes entrar a este link para ver un Pluging de ejemplo
  • Data - EmmaPluginData.kts

    Clase de datos que contiene la información que será entregada por el plugin una vez cargado. Esta información puede ser usada para renderizar vistas o mostrar contenido personalizado al usuario.

    EmmaPluginData.kts
    class EmmaPluginData(
        val title: String,
        val body: String,
        val custom: String
    )
    
  • Sealed Class - EmmaPluginState.kts

    Clase sellada que representa los diferentes estados posibles del plugin. Es útil para manejar el flujo de estados

    EmmaPluginState.kts
    sealed class EmmaPluginState {
      object Loading : EmmaPluginState()
      data class Success(val data: EmmaPluginData) : EmmaPluginState()
      data class Error(val message: String) : EmmaPluginState()
      object Closed : EmmaPluginState()
    }
    
  • UI - EmmaPluginViewModel.kts

    Clase que implementa la interfaz EmmaPluginCallback y expone el estado del plugin mediante un LiveData, permitiendo que la capa de UI observe los cambios y actualice su contenido en función del estado actual.

    EmmaPluginViewModel.kts
    class EmmaPluginViewModel : ViewModel(), EmmaPluginCallback {
    
      private val _pluginState = MutableLiveData<EmmaPluginState>()
      val pluginState: LiveData<EmmaPluginState> = _pluginState
    
      override fun onEmmaPluginLoaded(pluginData: EmmaPluginData) {
          _pluginState.value = EmmaPluginState.Success(pluginData)
      }
    
      override fun onEmmaPluginError(error: String) {
          _pluginState.value = EmmaPluginState.Error(error)
      }
    
      override fun onEmmaPluginClosed() {
          _pluginState.value = EmmaPluginState.Closed
      }
    
      override fun onEmmaPluginClicked(pluginData: EmmaPluginData) {
          // Puedes manejar acciones personalizadas aquí si es necesario
      }
    }
    
  • UI - PluginRenderer.kts

    Clase encargada de renderizar visualmente el contenido del plugin en pantalla. Toma los datos EmmaPluginData y los inserta dentro de un contenedor FrameLayout inflando un layout XML.

    PluginRenderer.kts
    class PluginRenderer(
      private val context: Context,
      private val container: FrameLayout
    ) {
    
      fun render(data: EmmaPluginData, onClick: (() -> Unit)? = null) {
          Log.d("PluginRenderer", "Renderizando plugin con título: ${data.title}")
    
          // Limpia el contenedor
          container.removeAllViews()
    
          // Infla el layout sin adjuntarlo inmediatamente
          val view: View = LayoutInflater.from(context)
              .inflate(R.layout.emma_plugin_container, container, false)
    
          // Asigna los textos recibidos
          view.findViewById<TextView>(R.id.plugin_title)?.text = data.title
          view.findViewById<TextView>(R.id.plugin_body)?.text = data.body
          view.findViewById<TextView>(R.id.plugin_custom)?.text = data.custom
    
          // Configura el botón de acción
          view.findViewById<Button>(R.id.plugin_action_button)?.setOnClickListener {
              Log.d("PluginRenderer", "Botón del plugin clickeado")
              onClick?.invoke()
          }
    
          container.addView(view)
          container.visibility = View.VISIBLE
          container.requestLayout()
          container.invalidate()
      }
    }
    
  • MainActivity

    Esta actividad conecta con CustomEmmaPlugin para mostrar y gestionar el contenido dinámico que proviene del SDK de EMMA.

    Se integra con el EmmaPluginViewModel para observar los cambios de estado del plugin (carga, error, cierre, clic), y renderiza la interfaz del plugin utilizando la clase PluginRenderer.

    MainActivity.kts
    class MainActivity : AppCompatActivity(), EmmaPluginCallback {
    
      private lateinit var customEmmaPlugin: CustomEmmaPlugin
      private val emmaPluginViewModel: EmmaPluginViewModel by viewModels()
    
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
    
          setupEmmaPlugin()
          observePluginStates()
    
          findViewById<View>(R.id.btn_test_plugin).setOnClickListener {
              Toast.makeText(this, "Probando plugin de EMMA...", Toast.LENGTH_SHORT).show()
    
              val request = EMMANativeAdRequest().apply {
                  templateId = customEmmaPlugin.id
              }
    
              try {
                  EMMA.getInstance().getInAppMessage(request)
              } catch (e: Exception) {
                  emmaPluginViewModel.onEmmaPluginError("Error al cargar el plugin: ${e.message}")
              }
          }
      }
    
      private fun setupEmmaPlugin() {
          customEmmaPlugin = CustomEmmaPlugin().apply {
              setPluginCallback(this@MainActivity)
          }
      }
    
      override fun onEmmaPluginLoaded(pluginData: EmmaPluginData) {
          emmaPluginViewModel.onEmmaPluginLoaded(pluginData)
      }
    
      override fun onEmmaPluginError(error: String) {
          emmaPluginViewModel.onEmmaPluginError(error)
      }
    
      override fun onEmmaPluginClosed() {
          emmaPluginViewModel.onEmmaPluginClosed()
      }
    
      override fun onEmmaPluginClicked(pluginData: EmmaPluginData) {
          Toast.makeText(this, "Plugin clickeado: ${pluginData.title}", Toast.LENGTH_SHORT).show()
      }
    
      private fun observePluginStates() {
          emmaPluginViewModel.pluginState.observe(this, Observer { state ->
              when (state) {
                  is EmmaPluginState.Success -> showPluginData(state.data)
                  is EmmaPluginState.Error -> showError(state.message)
                  is EmmaPluginState.Closed -> clearPlugin()
                  is EmmaPluginState.Loading -> showLoading()
              }
          })
      }
    
      private fun showPluginData(pluginData: EmmaPluginData) {
          val pluginContainer = findViewById<FrameLayout>(R.id.emma_plugin_container)
          pluginContainer.visibility = View.VISIBLE
    
          val renderer = PluginRenderer(this, pluginContainer)
          renderer.render(pluginData) {
              Toast.makeText(this, "Plugin clickeado: ${pluginData.title}", Toast.LENGTH_SHORT).show()
              customEmmaPlugin.handleClick(pluginData)
          }
      }
    
      private fun clearPlugin() {
          val pluginContainer = findViewById<FrameLayout>(R.id.emma_plugin_container)
          pluginContainer.removeAllViews()
          Toast.makeText(this, "Plugin cerrado", Toast.LENGTH_SHORT).show()
      }
    
      private fun showError(message: String) {
          Toast.makeText(this, "Error: $message", Toast.LENGTH_SHORT).show()
      }
    
      private fun showLoading() {
          Toast.makeText(this, "Cargando plugin...", Toast.LENGTH_SHORT).show()
      }
    }
    
  • Data - ExampleApplication.kts

    Para integrar un plugin es necesario añadirlo en el sdk después del inicio de sesión, para ello es necesario utilizar el método addInAppPlugin.

    ExampleApplication.kts
    import android.app.Application
    import io.emma.android.EMMA
    
    class ExampleApplication : Application() {
    
        override fun onCreate() {
            super.onCreate()
    
            val configuration = EMMA.Configuration.Builder(this)
                .setSessionKey("TU_SESION_KEY")
                .trackScreenEvents(false)
                .setDebugActive(BuildConfig.DEBUG)
                .build()
    
            EMMA.getInstance().startSession(configuration)
            EMMA.getInstance().addInAppPlugins(CustomEmmaPlugin()) 
        }
    }