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
Componente | Requerimiento |
---|---|
Sistema Operativo | Windows 10 o superior |
IDE | Android Studio (última versión recomendada 2024.3.1) |
Kotlin | Versión: 2.0.21 |
Gradle | Versión: 8.11.1 |
SDK EMMA | Versió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") })
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")
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.
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:
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" />
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.
Inicializar la librería
En tu clase Application, añade lo siguiente:
ExampleApplication.ktsimport 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 }
Omitir este paso podría generar un error de la clase BuildConfig.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.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.
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)
.
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.
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.
<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.
# 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.** { *; }
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
./gradlew assembleRelease
Verifica que haya compilado correctamente, debe aparecer un mensaje similar a esto:

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
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.
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.
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>
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
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"))
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.integrationkotlinimport 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.
Kotlinoverride 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.Kotlinfun 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:
Kotlinfun 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()
.KotlinEMMA.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()
.KotlinEMMA.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()
.KotlinEMMA.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()
.Kotlinfun 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.
UI – BehaviorViewModel.kt
Encapsula la lógica del evento personalizado para ser reutilizada fácilmente.
Dominio – BehaviorTracker.kt
Define el contrato que implementará la capa de datos.
Data – BehaviorTrackerImpl.kt
Aquí es donde se construye el EMMAEventRequest, se agregan los atributos y finalmente se envía el evento al SDK.
Identificador de EMMA
Podemos recuperar el identificador interno de EMMA con el método
getUserID()
:Kotlinclass 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:
KotlinEMMA.getInstance().getDeviceId()
Identificador de usuario del cliente (Customer ID)
Para enviar el customer ID independientemente del login/registro usa el siguiente método:
KotlinEMMA.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()
:Kotlinclass 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
:Kotlinclass 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 } } } }
La documentación oficial da como ejemplo el siguiente método: Puedes consultar más información sobre los eventos personalizados aquí.
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)
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.ktclass 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 oficialInterface – NativeAdRepository.kt
Crear interface que actúe como puente entre la UI/ViewModel y la capa de datos (NativeAdDataSource)
NativeAdDataSourceInterface.ktinterface 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.ktclass 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.ktclass 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.ktfun 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.ktfun 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.ktprivate 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.ktprivate 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.ktprivate 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.
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.
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.
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.
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.ktclass 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 oficialInterface - CouponDataSourceInterface.kt
Crear interface que actúe como puente entre la UI/ViewModel y la capa de datos (CouponDataSource)
CouponDataSourceInterface.ktinterface 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.ktclass 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.ktclass 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.ktfun 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.ktprivate 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.ktsclass 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.ktsclass 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() } }
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.ktsclass 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.ktssealed 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.ktsclass 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.ktsclass 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.ktsclass 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.ktsimport 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()) } }