Tutoriales

MQTT en Android con Kotlin

Crea un cliente MQTT en Android con Kotlin: HiveMQ client con coroutines, opciones de Paho, ciclo de vida, Doze y límites en segundo plano, TLS y pruebas.

Actualizado · 9 min de lectura

MQTT en Android con Kotlin

Para usar MQTT en una app Android con Kotlin, añade un cliente MQTT para Java que esté mantenido (hoy el que mejor encaja es HiveMQ MQTT Client), envuélvelo en un repositorio que exponga el estado de la conexión y los mensajes como flows de Kotlin y vincula la conexión al ciclo de vida de tu app en lugar de intentar mantenerla viva para siempre. Este tutorial construye exactamente eso, explica qué hace Android con los sockets de larga duración en segundo plano y muestra cómo probar el resultado contra un broker real.

Elegir una librería cliente

LibreríaEstado en AndroidNotas
HiveMQ MQTT ClientFunciona, mantenimiento activoMQTT 3.1.1 y 5; la API asíncrona devuelve CompletableFuture, fácil de conectar con coroutines
Eclipse Paho Java (mqttv3)FuncionaCliente MQTT 3.1.1 maduro con callbacks; tú gestionas los hilos
Eclipse Paho Android ServiceEvitarSin mantenimiento, falla en versiones recientes de Android

Los ejemplos usan el cliente de HiveMQ. La misma arquitectura funciona con Paho Java; solo cambian las llamadas de conexión y suscripción. Si conoces el cliente de la JVM, MQTT en Java con HiveMQ MQTT Client cubre su API en profundidad.

Configuración del proyecto

app/build.gradle.kts
android {
    defaultConfig {
        minSdk = 24
    }
}

dependencies {
    implementation("com.hivemq:hivemq-mqtt-client:1.3.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
    implementation("androidx.lifecycle:lifecycle-process:2.8.7")
}
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

El cliente depende de APIs de Java 8 como CompletableFuture, disponibles desde la API 24. Si R8 informa de clases que faltan de la dependencia Netty del cliente durante un build de release, añade las reglas keep de la documentación Android del cliente.

Un repositorio con coroutines y flows

Mantén exactamente un cliente por app, propiedad de un objeto de larga duración, no de una Activity, que se recrea en cada rotación. El repositorio de abajo expone el estado de la conexión como StateFlow y los mensajes entrantes como SharedFlow:

MqttRepository.kt
import com.hivemq.client.mqtt.MqttClient
import com.hivemq.client.mqtt.datatypes.MqttQos
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.future.await

data class MqttMessage(val topic: String, val payload: String)

sealed interface ConnState {
    data object Disconnected : ConnState
    data object Connected : ConnState
    data class Failed(val reason: String) : ConnState
}

class MqttRepository(clientId: String) {

    private val _state = MutableStateFlow<ConnState>(ConnState.Disconnected)
    val state: StateFlow<ConnState> = _state

    private val _messages = MutableSharedFlow<MqttMessage>(extraBufferCapacity = 64)
    val messages: SharedFlow<MqttMessage> = _messages

    private val client: Mqtt5AsyncClient = MqttClient.builder()
        .useMqttVersion5()
        .identifier(clientId)
        .serverHost("broker.connectmq.com")
        .serverPort(8883)
        .sslWithDefaultConfig()
        .automaticReconnectWithDefaultConfig()
        .addConnectedListener { _state.value = ConnState.Connected }
        .addDisconnectedListener { _state.value = ConnState.Failed(it.cause.message ?: "disconnected") }
        .buildAsync()

    suspend fun connect(user: String, pass: String) {
        client.connectWith()
            .simpleAuth().username(user).password(pass.toByteArray()).applySimpleAuth()
            .keepAlive(60)
            .send()
            .await()
    }

    suspend fun subscribe(filter: String) {
        client.subscribeWith()
            .topicFilter(filter)
            .qos(MqttQos.AT_LEAST_ONCE)
            .callback { p ->
                _messages.tryEmit(MqttMessage(p.topic.toString(), String(p.payloadAsBytes)))
            }
            .send()
            .await()
    }

    suspend fun publish(topic: String, payload: String) {
        client.publishWith()
            .topic(topic)
            .qos(MqttQos.AT_LEAST_ONCE)
            .payload(payload.toByteArray())
            .send()
            .await()
    }

    suspend fun disconnect() {
        client.disconnect().await()
        _state.value = ConnState.Disconnected
    }
}

await() viene de kotlinx.coroutines.future, que forma parte de la librería principal de coroutines, y convierte cada CompletableFuture en una llamada suspendida. El callback de la suscripción se ejecuta en el hilo propio del cliente; tryEmit entrega el mensaje sin bloquearlo. Después, un ViewModel simplemente recoge repository.messages y repository.state en viewModelScope.

Vincular la conexión al ciclo de vida de la app

El patrón fiable más sencillo para la mayoría de apps: conectar cuando la app pasa a primer plano y desconectar cuando pasa a segundo plano. ProcessLifecycleOwner informa de esto para todo el proceso, no por Activity, así que las rotaciones y los cambios de pantalla no provocan reconexiones:

App.kt
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

class App : android.app.Application() {

    lateinit var mqtt: MqttRepository

    override fun onCreate() {
        super.onCreate()
        mqtt = MqttRepository(clientId = InstallId.get(this))

        ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onStart(owner: LifecycleOwner) {
                owner.lifecycleScope.launch {
                    runCatching {
                        mqtt.connect(BuildConfig.MQTT_USER, BuildConfig.MQTT_PASS)
                        mqtt.subscribe("testmqtt/android/demo/#")
                    }
                }
            }

            override fun onStop(owner: LifecycleOwner) {
                owner.lifecycleScope.launch { runCatching { mqtt.disconnect() } }
            }
        })
    }
}

InstallId representa tu propio helper que devuelve el UUID guardado por instalación. No incluyas credenciales reales del broker en BuildConfig en una app de producción: dale a cada usuario o dispositivo sus propias credenciales desde tu backend. Para pruebas rápidas, los placeholders están bien.

Qué hace Android en segundo plano

Una conexión TCP en una app en segundo plano no sobrevive mucho tiempo, y es así por diseño:

  • Doze y App Standby suspenden el acceso a la red en dispositivos inactivos y apps poco usadas. Tus PINGREQ de keep alive se detienen y el broker cierra la conexión tras 1,5 × el intervalo de keep alive.
  • Los procesos en caché pueden congelarse o eliminarse en cualquier momento para liberar memoria; el socket muere con ellos.
  • Los ahorradores de batería de los fabricantes en muchos dispositivos son más estrictos que Android estándar y pueden matar directamente el trabajo en segundo plano.
  • Los foreground services mantienen vivo el proceso, pero requieren una notificación visible y un tipo de servicio declarado en Android 14+, y Android 15 limita cuánto tiempo pueden ejecutarse los foreground services dataSync.

Las consecuencias prácticas: no prometas a los usuarios entrega MQTT en tiempo real con la app cerrada. Si algo importante debe llegar a una app cerrada (una alarma, un mensaje de chat), envía una notificación push mediante Firebase Cloud Messaging desde tu backend y deja que abra la app, que entonces se reconecta y sincroniza. Usa un foreground service solo cuando el usuario esté haciendo algo continuo, como seguimiento en vivo durante un trayecto. Desconexiones de Android en segundo plano profundiza en el diagnóstico de cada caso.

Sesiones persistentes: ponerse al día al volver

Cuando la app vuelve a primer plano, puede recibir los mensajes QoS 1 que se perdió, si el broker conservó su sesión. Configura cleanStart(false) y un sessionExpiryInterval en connectWith(), y mantén estable el client ID. El broker encola entonces los mensajes de las suscripciones mientras la app no está, hasta el tiempo de expiración. Elige un intervalo acorde a cuánto tiempo siguen siendo útiles los datos perdidos; un día de lecturas de sensores en cola llegando de golpe rara vez es lo que quiere la UI. El endpoint compartido de pruebas no conserva sesiones offline, así que pruébalo con tu propia instancia privada de broker. Lee sesiones persistentes de MQTT para conocer las reglas exactas.

Cambios de red y keep alive

Los teléfonos cambian constantemente entre Wi-Fi y datos móviles, y cada cambio mata en silencio las conexiones TCP existentes. La reconexión automática del cliente se encarga de ello, pero la detección es tan rápida como tu keep alive: con 60 segundos, una conexión muerta puede pasar desapercibida hasta 90 segundos. Para reaccionar antes, registra un ConnectivityManager.NetworkCallback y lanza una reconexión desde onAvailable. En móvil, mantén el intervalo entre 30 y 120 segundos: más corto gasta batería, más largo retrasa la detección. Keep alive de MQTT explica los tiempos.

Pruébalo en un dispositivo

  1. Ejecuta la app en un dispositivo o emulador y observa cómo state pasa a Connected.
  2. Abre el cliente MQTT online en tu ordenador, conéctate al mismo broker y publica en testmqtt/android/demo/hello. El mensaje debería aparecer en la app.
  3. Publica desde la app y suscríbete en el navegador a testmqtt/android/# para confirmar la otra dirección.
  4. Pon la app en segundo plano, espera más que el timeout de keep alive y vuelve a abrirla. Deberías ver un nuevo estado Connected y, con una sesión persistente, los mensajes publicados mientras tanto.
  5. Activa el modo avión unos segundos y confirma que el cliente se reconecta solo.

Mira los mensajes de tu app Android

¿Te conectas desde el emulador a un broker en tu propio ordenador? Usa 10.0.2.2 en lugar de localhost. Para más comprobaciones desde el dispositivo, consulta probar un broker MQTT desde Android.

MQTT en Android funciona mejor cuando aceptas las reglas de la plataforma: un cliente en un repositorio de larga duración, una conexión que sigue el ciclo de vida de la app, sesiones persistentes para ponerse al día, notificaciones push para lo urgente mientras la app está cerrada y TLS en todas partes. Constrúyelo una vez y cada pantalla de tu app simplemente recogerá un flow.

Preguntas frecuentes

¿Debería seguir usando Eclipse Paho Android Service?

No en apps nuevas. La librería org.eclipse.paho.android.service lleva años sin mantenimiento y falla en versiones recientes de Android (por ejemplo, por los requisitos de mutabilidad de PendingIntent desde Android 12). Usa HiveMQ MQTT Client o el cliente Paho Java normal dentro de la arquitectura de tu app.

¿Puede una app Android mantener una conexión MQTT abierta en segundo plano?

Solo con un foreground service y una notificación visible, e incluso así Doze, los ahorradores de batería de los fabricantes y las nuevas reglas de foreground services pueden interrumpirla. Para la mayoría de apps, conéctate mientras la app está visible y usa notificaciones push (FCM) para despertarla ante eventos importantes.

¿Por qué mi conexión MQTT funciona por Wi-Fi pero no con datos móviles?

Algunas redes móviles y Wi-Fi corporativas bloquean puertos no HTTP como 1883 y 8883. Conéctate por WebSockets seguros en el puerto 443 o 8084 para comprobar si el problema es el puerto, y usa siempre TLS en redes que no controlas.