Tutoriels IoT

Client MQTT Android en Kotlin : se connecter, s'abonner et notifier

Créez une application Android en Kotlin qui se connecte à un broker MQTT sur TLS, s'abonne à des topics d'alarme, déclenche des notifications système et se reconnecte correctement lorsque l'application passe du premier plan à l'arrière-plan.

Téléphone Android affichant une notification d'alarme provenant d'une application Android MQTT Kotlin connectée à un broker cloud
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Créer un client MQTT Android en Kotlin est simple au départ, et étonnamment facile à rater : la connexion doit survivre aux changements de réseau, respecter le cycle de vie de l'application et éviter de vider la batterie. Dans ce tutoriel, vous allez créer une application Kotlin qui se connecte en TLS avec MQTT 5, s'abonne à des topics d'alarme, déclenche des notifications Android et se reconnecte en tenant compte du cycle de vie. Elle fonctionne d'abord sur le serveur MQTT public gratuit broker.connectmq.com, réservé aux tests, puis sur un broker MQTT managé ConnectMQ privé.

Choisir une bibliothèque MQTT pour Android

Le choix de la bibliothèque compte plus sur Android que partout ailleurs, car de nombreux anciens tutoriels renvoient vers du code qui ne fonctionne plus sur les versions récentes d'Android.

Bibliothèque Statut pour les nouvelles applications Android Remarques
Eclipse Paho Android Service Déconseillé Non maintenu ; problèmes connus avec les versions récentes d'Android (limites des services en arrière-plan, flags de PendingIntent)
Client Java Eclipse Paho (mqttv3) Utilisable Client Java simple ; vous gérez vous-même le threading et le cycle de vie ; MQTT 3.1.1 uniquement
HiveMQ MQTT Client Recommandé ici Maintenu, MQTT 5 et 3.1.1, API asynchrone, reconnexion automatique intégrée avec réabonnement

Ce guide utilise HiveMQ MQTT Client, une bibliothèque Java généraliste qui fonctionne sur Android. Sa documentation sur la page GitHub de HiveMQ MQTT Client détaille la configuration propre à Android.

Configuration du projet

Créez un nouveau projet dans Android Studio avec une Empty Activity (Compose). Définissez minSdk à 26 ou plus : cela permet de créer des canaux de notification sans vérification de version et évite un travail de desugaring supplémentaire pour les API Java 8 de la bibliothèque.

Dans app/build.gradle.kts :

android {
    defaultConfig {
        minSdk = 26
    }
    packaging {
        resources {
            // Netty, used internally by the client, ships duplicate metadata files
            excludes += setOf("META-INF/INDEX.LIST", "META-INF/io.netty.versions.properties")
        }
    }
}

dependencies {
    implementation("com.hivemq:hivemq-mqtt-client:1.3.3") // check Maven Central for the latest 1.3.x
    implementation("androidx.lifecycle:lifecycle-process:2.8.7")
}

Sans les exclusions packaging, le build échoue généralement avec une erreur « more than one file was found ». Dans AndroidManifest.xml, ajoutez les permissions :

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

POST_NOTIFICATIONS est une permission d'exécution sur Android 13 et versions ultérieures ; vous la demanderez depuis l'activité.

La classe client Android MQTT Kotlin

Créez MqttRepository.kt. Elle détient un client unique pour toute l'application, expose l'état de connexion et la dernière alarme sous forme de StateFlow, et publie une notification pour chaque message d'alarme.

package com.example.mqttalarms

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.hivemq.client.mqtt.MqttClient
import com.hivemq.client.mqtt.datatypes.MqttQos
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient
import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger

class MqttRepository(private val context: Context) {

    companion object {
        const val PREFIX = "connectmq/demo/a7k2"
        private const val CHANNEL_ID = "alarms"
        private const val TAG = "MqttRepository"
    }

    private val _connected = MutableStateFlow(false)
    val connected: StateFlow<Boolean> = _connected

    private val _lastAlarm = MutableStateFlow("No alarms yet")
    val lastAlarm: StateFlow<String> = _lastAlarm

    private val notificationId = AtomicInteger(1000)

    private val clientId: String by lazy {
        val prefs = context.getSharedPreferences("mqtt", Context.MODE_PRIVATE)
        prefs.getString("client_id", null) ?: ("android-" + UUID.randomUUID().toString().take(12))
            .also { prefs.edit().putString("client_id", it).apply() }
    }

    private val client: Mqtt5AsyncClient by lazy {
        MqttClient.builder()
            .useMqttVersion5()
            .identifier(clientId)
            .serverHost("broker.connectmq.com")
            .serverPort(8883)
            .sslWithDefaultConfig()
            .automaticReconnect()
                .initialDelay(1, TimeUnit.SECONDS)
                .maxDelay(30, TimeUnit.SECONDS)
                .applyAutomaticReconnect()
            .addConnectedListener { _connected.value = true; Log.i(TAG, "connected") }
            .addDisconnectedListener { ctx ->
                _connected.value = false
                Log.w(TAG, "disconnected: ${ctx.cause.message} (source ${ctx.source})")
            }
            .buildAsync()
    }

    init {
        val channel = NotificationChannel(CHANNEL_ID, "Device alarms",
            NotificationManager.IMPORTANCE_HIGH)
        context.getSystemService(NotificationManager::class.java)
            .createNotificationChannel(channel)
    }

    fun connect() {
        if (client.state.isConnectedOrReconnect) return
        client.connectWith()
            .cleanStart(true)
            .keepAlive(30)
            .send()
            .whenComplete { connAck, error ->
                if (error != null) {
                    Log.e(TAG, "connect failed", error)
                } else {
                    Log.i(TAG, "CONNACK ${connAck.reasonCode}")
                    subscribe()
                }
            }
    }

    private fun subscribe() {
        client.subscribeWith()
            .topicFilter("$PREFIX/+/alarm")
            .qos(MqttQos.AT_LEAST_ONCE)
            .callback(::onAlarm)
            .send()
            .whenComplete { subAck, error ->
                if (error != null) Log.e(TAG, "subscribe failed", error)
                else Log.i(TAG, "SUBACK ${subAck.reasonCodes}")
            }
    }

    private fun onAlarm(publish: Mqtt5Publish) {
        // Runs on the client's network thread: no UI work here, only thread-safe calls
        val device = publish.topic.levels.getOrNull(3) ?: "unknown"
        val text = String(publish.payloadAsBytes, Charsets.UTF_8)
        _lastAlarm.value = "$device: $text"
        showNotification(device, text)
    }

    private fun showNotification(device: String, text: String) {
        val nm = NotificationManagerCompat.from(context)
        if (!nm.areNotificationsEnabled()) return
        val notification = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(android.R.drawable.stat_sys_warning)
            .setContentTitle("Alarm on $device")
            .setContentText(text)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)
            .build()
        try {
            nm.notify(notificationId.incrementAndGet(), notification)
        } catch (e: SecurityException) {
            Log.w(TAG, "notification permission missing")
        }
    }

    fun disconnect() {
        if (client.state.isConnected) client.disconnect()
    }
}

Les points qui font gagner du temps de débogage :

  • Un seul client par application. Créer un client par écran entraîne des connexions en double avec le même client ID, qui s'expulsent mutuellement du broker en boucle.
  • Un client ID stable stocké dans SharedPreferences, généré une seule fois par installation.
  • La reconnexion automatique avec backoff gère les bascules entre Wi-Fi et réseau mobile ainsi que les tunnels. Le client HiveMQ se réabonne automatiquement si le broker n'a plus la session : vous ne perdez donc pas votre abonnement après une reconnexion.
  • Le callback de message s'exécute hors du thread principal. Mettre à jour un StateFlow et publier une notification y sont sans danger ; manipuler des vues ne l'est pas.
  • Un disconnect() initié par l'utilisateur ne déclenche pas la reconnexion automatique, ce qui est exactement le comportement voulu lorsque l'application passe en arrière-plan.

Une connexion consciente du cycle de vie

Garder un socket MQTT ouvert alors que l'application est invisible vide la batterie, et le système le coupe de toute façon. Liez la connexion au cycle de vie visible de l'application avec ProcessLifecycleOwner, qui signale quand l'application entière (et non une seule activité) passe du premier plan à l'arrière-plan.

package com.example.mqttalarms

import android.app.Application
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner

class AlarmApp : Application() {
    lateinit var mqtt: MqttRepository
        private set

    override fun onCreate() {
        super.onCreate()
        mqtt = MqttRepository(this)
        ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onStart(owner: LifecycleOwner) = mqtt.connect()
            override fun onStop(owner: LifecycleOwner) = mqtt.disconnect()
        })
    }
}

Enregistrez-la avec android:name=".AlarmApp" sur l'élément <application>. La rotation de l'écran et le passage d'une de vos activités à une autre ne provoquent plus de déconnexion, car le cycle de vie au niveau du processus ne s'arrête que lorsque l'application dans son ensemble quitte l'écran.

L'interface et la permission de notification

Une activité Compose minimale qui demande la permission de notification et affiche l'état :

package com.example.mqttalarms

import android.Manifest
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    private val requestPermission =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (Build.VERSION.SDK_INT >= 33) {
            requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
        val mqtt = (application as AlarmApp).mqtt
        setContent {
            MaterialTheme {
                val connected by mqtt.connected.collectAsState()
                val alarm by mqtt.lastAlarm.collectAsState()
                Column(Modifier.padding(24.dp)) {
                    Text(if (connected) "Connected to broker" else "Disconnected")
                    Text("Last alarm: $alarm", style = MaterialTheme.typography.titleMedium)
                }
            }
        }
    }
}

Tester avec un appareil simulé

Lancez l'application, autorisez les notifications, puis publiez une alarme depuis un terminal, comme le ferait un appareil :

mosquitto_pub -h broker.connectmq.com -p 1883 -i android-test-a7k2 \
  -t connectmq/demo/a7k2/pump3/alarm -q 1 -m "Pressure above 8 bar"

Une notification heads-up apparaît et l'écran affiche l'alarme. Activez le mode avion quelques secondes : le statut passe à « Disconnected », puis revient une fois le réseau rétabli, et les nouvelles alarmes arrivent sans redémarrer l'application. Utilisez votre propre préfixe de topic à la place de a7k2, et n'oubliez pas que l'espace public est lisible par tous. D'autres astuces en CLI figurent dans les exemples mosquitto_pub et mosquitto_sub.

Concevoir les topics d'alarme

Les notifications ne valent que par les messages qui les alimentent. Gardez les payloads d'alarme courts et lisibles, ou envoyez un JSON compact avec un champ de sévérité et construisez le texte dans l'application. Publiez les alarmes en QoS 1 pour qu'un bref accroc réseau ne les fasse pas disparaître, et évitez le flag retain pour les événements ponctuels : une alarme retained déclencherait une nouvelle notification à chaque reconnexion de l'application. Si vous devez afficher l'état actuel des alarmes à l'écran, publiez-le séparément sur un topic retained comme .../pump3/alarm/active, et réservez le topic de notification aux transitions.

Alertes en arrière-plan : choisir la bonne architecture

Comme l'application se déconnecte en arrière-plan, elle ne notifie que lorsqu'elle est ouverte. Pour les alertes lorsque l'application est fermée, choisissez l'une de ces options :

Approche Fonctionnement Compromis
Subscriber côté serveur et push (recommandé) Un service backend s'abonne aux topics d'alarme et envoie des notifications push Le plus économe en batterie et le plus fiable ; nécessite un petit backend
Foreground service L'application maintient la connexion MQTT dans un service avec une notification permanente visible Fonctionne sans backend, mais les versions récentes d'Android restreignent les types et la durée des foreground services, et les utilisateurs voient une notification permanente
Vérification périodique avec WorkManager Connexion brève selon un planning pour lire l'état retained des alarmes Simple, mais retardé de plusieurs minutes et soumis aux optimisations de batterie

Pour un backend .NET, le modèle du service d'arrière-plan MQTT avec ASP.NET Core convient bien au subscriber côté serveur. Les alternatives multiplateformes au Kotlin natif sont traitées dans Flutter MQTT et React Native MQTT.

Dépannage

  • Erreur de build sur des fichiers META-INF en double : ajoutez les exclusions packaging présentées plus haut.
  • SSLHandshakeException : horloge de l'appareil erronée, ou trust store Android très ancien ; le broker utilise un certificat Let's Encrypt.
  • Connexion puis déconnexion en boucle : un autre client (souvent un émulateur et un téléphone) utilise le même client ID.
  • Aucune notification sur Android 13+ : l'utilisateur a refusé POST_NOTIFICATIONS, ou le canal a été rendu silencieux dans les paramètres système.
  • Not authorized sur un broker privé : le client ID doit être celui émis avec vos identifiants.

Configuration de production avec un broker privé

Pour de vrais appareils et de vrais utilisateurs, connectez-vous à un broker privé et authentifié. ConnectMQ génère un nom d'utilisateur, un mot de passe et un client ID lorsque vous créez un broker, et lie le client ID à ces identifiants. Remplacez les lignes du builder :

MqttClient.builder()
    .useMqttVersion5()
    .identifier("YOUR_CLIENT_ID")
    .serverHost("YOUR_BROKER_HOST")
    .serverPort(8883)
    .sslWithDefaultConfig()
    .simpleAuth()
        .username("YOUR_USERNAME")
        .password("YOUR_PASSWORD".toByteArray())
        .applySimpleAuth()
    // ... reconnect and listeners as before
    .buildAsync()

N'intégrez pas en dur un identifiant partagé dans un APK publié ; provisionnez des identifiants par utilisateur ou par installation depuis votre backend et stockez-les avec l'Android Keystore. Les bonnes pratiques de sécurité MQTT traitent ce sujet en détail. Le serveur MQTT public gratuit est l'endroit idéal pour prototyper, et un broker MQTT managé celui du trafic de production. Pour les options de bibliothèques sur d'autres plateformes, consultez le guide des bibliothèques client MQTT.

Démarrer avec ConnectMQ

Créez votre broker privé sur app.connectmq.com avec le plan Serverless gratuit, sans carte bancaire. Branchez les identifiants générés dans le builder, puis utilisez le client MQTT intégré au navigateur du portail pour publier des alarmes de test vers votre téléphone, et ses règles d'alerte à seuil ainsi que son journal d'alertes pour garder un œil sur les valeurs des appareils.

Questions fréquentes

Eclipse Paho est-il toujours un bon choix pour Android ?

Paho Android Service n'est plus maintenu et pose problème sur les versions récentes d'Android : évitez-le pour les nouvelles applications. Le client Java Paho classique fonctionne mais ne prend en charge que MQTT 3.1.1 ; HiveMQ MQTT Client est une option plus complète pour Kotlin.

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

Seulement avec un foreground service et une notification visible, et les versions récentes d'Android restreignent encore davantage cette pratique. Pour les alertes en arrière-plan, un subscriber côté serveur qui envoie des notifications push est plus fiable.

HiveMQ MQTT Client prend-il en charge les coroutines Kotlin ?

Il expose des API asynchrone (CompletableFuture), réactive et bloquante. Vous pouvez appeler l'API asynchrone depuis des coroutines grâce à l'extension await() de kotlinx-coroutines-jdk8.

Quel port un client MQTT Android doit-il utiliser ?

Utilisez TLS sur le port 8883. Si un réseau n'autorise que le HTTPS, utilisez WebSocket sécurisé sur le port 8084 avec le chemin /mqtt, que le client HiveMQ prend en charge via sa configuration WebSocket.

Articles liés

Les meilleurs outils CLI MQTT pour tester, scripter et déboguer

Un comparatif pratique de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI MQTT.js, curl et mqttui, avec le même test exécuté sur le serveur MQTT public gratuit broker.connectmq.com pour choisir le bon outil pour le scripting et le débogage.

9 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.