Tutoriels

MQTT sur Android avec Kotlin

Créez un client MQTT Android en Kotlin : client HiveMQ et coroutines, options Paho, cycle de vie, Doze et limites en arrière-plan, TLS et tests sur appareil.

Mis à jour · 9 min de lecture

MQTT sur Android avec Kotlin

Pour utiliser MQTT dans une application Android en Kotlin, ajoutez un client MQTT Java maintenu (le HiveMQ MQTT Client est aujourd'hui le plus adapté), encapsulez-le dans un repository qui expose l'état de connexion et les messages sous forme de flows Kotlin, et liez la connexion au cycle de vie de l'application au lieu d'essayer de la maintenir en vie indéfiniment. Ce tutoriel construit exactement cela, explique ce qu'Android fait des sockets longue durée en arrière-plan et montre comment tester le résultat contre un vrai broker.

Choisir une bibliothèque cliente

BibliothèqueStatut sur AndroidRemarques
HiveMQ MQTT ClientFonctionne, activement maintenuMQTT 3.1.1 et 5 ; l'API asynchrone renvoie des CompletableFuture, faciles à relier aux coroutines
Eclipse Paho Java (mqttv3)FonctionneClient MQTT 3.1.1 mature à callbacks ; vous gérez vous-même les threads
Eclipse Paho Android ServiceÀ éviterNon maintenu, casse sur les versions récentes d'Android

Les exemples utilisent le client HiveMQ. La même architecture fonctionne avec Paho Java ; seuls les appels de connexion et d'abonnement changent. Si vous connaissez ce client côté JVM, MQTT en Java avec le HiveMQ MQTT Client détaille son API.

Configuration du projet

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" />

Le client s'appuie sur des API Java 8 comme CompletableFuture, disponibles à partir de l'API 24. Si R8 signale des classes manquantes de la dépendance Netty du client lors d'un build release, ajoutez les règles keep indiquées dans la documentation Android du client.

Un repository avec coroutines et flows

Gardez exactement un client par application, détenu par un objet de longue durée, et non par une Activity, recréée à chaque rotation. Le repository ci-dessous expose l'état de connexion en StateFlow et les messages entrants en 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() provient de kotlinx.coroutines.future, qui fait partie de la bibliothèque coroutines principale, et transforme chaque CompletableFuture en appel suspendu. Le callback d'abonnement s'exécute sur le thread propre du client ; tryEmit transmet le message sans le bloquer. Un ViewModel collecte ensuite simplement repository.messages et repository.state dans viewModelScope.

Lier la connexion au cycle de vie de l'application

Le schéma fiable le plus simple pour la plupart des applications : se connecter quand l'application passe au premier plan, se déconnecter quand elle passe en arrière-plan. ProcessLifecycleOwner signale ces transitions pour tout le processus, pas par Activity : rotations et changements d'écran ne provoquent donc pas de reconnexion :

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 désigne votre propre helper qui renvoie l'UUID stocké par installation. N'embarquez pas de vrais identifiants de broker dans BuildConfig pour une application de production : donnez à chaque utilisateur ou appareil ses propres identifiants depuis votre backend. Pour des tests rapides, des placeholders suffisent.

Ce qu'Android fait en arrière-plan

Une connexion TCP dans une application en arrière-plan ne survit pas longtemps, et c'est voulu :

  • Doze et App Standby suspendent l'accès réseau des appareils inactifs et des applications peu utilisées. Vos PINGREQ de keep alive s'arrêtent et le broker ferme la connexion après 1,5 × l'intervalle de keep alive.
  • Les processus en cache peuvent être gelés ou tués à tout moment pour libérer de la mémoire ; le socket meurt avec eux.
  • Les économiseurs de batterie des constructeurs sont, sur de nombreux appareils, plus stricts qu'Android standard et peuvent tuer purement et simplement le travail en arrière-plan.
  • Les foreground services maintiennent le processus en vie, mais exigent une notification visible et un type de service déclaré sous Android 14+, et Android 15 limite la durée d'exécution des foreground services dataSync.

Conséquences pratiques : ne promettez pas aux utilisateurs une livraison MQTT en temps réel quand l'application est fermée. Si quelque chose d'important doit atteindre une application fermée (une alarme, un message de chat), envoyez une notification push via Firebase Cloud Messaging depuis votre backend et laissez-la ouvrir l'application, qui se reconnecte alors et se synchronise. N'utilisez un foreground service que lorsque l'utilisateur fait quelque chose de continu, comme un suivi en direct pendant un trajet. Les déconnexions Android en arrière-plan approfondit le diagnostic de chaque cas.

Sessions persistantes : rattraper le retard au retour

Quand l'application revient au premier plan, elle peut recevoir les messages QoS 1 manqués, à condition que le broker ait conservé sa session. Définissez cleanStart(false) et un sessionExpiryInterval dans connectWith(), et gardez un client ID stable. Le broker met alors en file les messages des abonnements pendant l'absence de l'application, jusqu'à l'expiration. Choisissez un intervalle adapté à la durée pendant laquelle les données manquées restent utiles ; une journée de mesures de capteurs arrivant d'un coup est rarement ce que veut l'interface. L'endpoint de test partagé ne conserve pas les sessions hors ligne : testez donc ceci avec votre propre instance de broker privée. Lisez les sessions persistantes MQTT pour les règles exactes.

Changements de réseau et keep alive

Les téléphones basculent constamment entre Wi-Fi et données mobiles, et chaque bascule tue silencieusement les connexions TCP existantes. La reconnexion automatique du client s'en charge, mais la détection n'est pas plus rapide que votre keep alive : avec 60 secondes, une connexion morte peut passer inaperçue jusqu'à 90 secondes. Pour réagir plus vite, enregistrez un ConnectivityManager.NetworkCallback et déclenchez une reconnexion depuis onAvailable. Sur mobile, gardez l'intervalle entre 30 et 120 secondes : plus court consomme de la batterie, plus long retarde la détection. Le keep alive MQTT explique les délais.

Tester sur un appareil

  1. Lancez l'application sur un appareil ou un émulateur et regardez state passer à Connected.
  2. Ouvrez le client MQTT en ligne sur votre ordinateur, connectez-vous au même broker et publiez sur testmqtt/android/demo/hello. Le message doit apparaître dans l'application.
  3. Publiez depuis l'application et abonnez-vous dans le navigateur à testmqtt/android/# pour confirmer l'autre sens.
  4. Mettez l'application en arrière-plan, attendez plus longtemps que le timeout de keep alive, puis ramenez-la. Vous devriez voir un nouvel état Connected et, avec une session persistante, les messages publiés entre-temps.
  5. Activez le mode avion quelques secondes et vérifiez que le client se reconnecte tout seul.

Observez les messages de votre application Android

Vous vous connectez depuis l'émulateur à un broker sur votre propre ordinateur ? Utilisez 10.0.2.2 au lieu de localhost. Pour d'autres vérifications côté appareil, voir tester un broker MQTT depuis Android.

MQTT sur Android fonctionne au mieux quand vous acceptez les règles de la plateforme : un client dans un repository de longue durée, une connexion qui suit le cycle de vie de l'application, des sessions persistantes pour rattraper le retard, des notifications push pour l'urgent quand l'application est fermée, et TLS partout. Construisez cela une fois, et chaque écran de votre application n'a plus qu'à collecter un flow.

Questions fréquentes

Faut-il encore utiliser Eclipse Paho Android Service ?

Pas pour une nouvelle application. La bibliothèque org.eclipse.paho.android.service n'est plus maintenue depuis des années et casse sur les versions récentes d'Android (par exemple à cause des exigences de mutabilité des PendingIntent depuis Android 12). Utilisez plutôt le HiveMQ MQTT Client ou le client Paho Java classique dans votre propre architecture.

Une application Android peut-elle garder une connexion MQTT ouverte en arrière-plan ?

Seulement avec un foreground service et une notification visible, et même dans ce cas Doze, les économiseurs de batterie des constructeurs et les nouvelles règles des foreground services peuvent l'interrompre. Pour la plupart des applications, connectez-vous tant que l'application est visible et utilisez les notifications push (FCM) pour la réveiller lors d'événements importants.

Pourquoi ma connexion MQTT fonctionne-t-elle en Wi-Fi mais pas en données mobiles ?

Certains réseaux mobiles et Wi-Fi d'entreprise bloquent les ports non HTTP comme 1883 et 8883. Connectez-vous via WebSockets sécurisés sur le port 443 ou 8084 pour vérifier si le port est en cause, et utilisez toujours TLS sur les réseaux que vous ne contrôlez pas.