IoT-Tutorials

Android MQTT Kotlin Client: Verbinden, abonnieren und benachrichtigen

Entwickeln Sie eine Android-App in Kotlin, die sich über TLS mit einem MQTT-Broker verbindet, Alarm-Topics abonniert, Systembenachrichtigungen auslöst und sich korrekt neu verbindet, wenn die App zwischen Vorder- und Hintergrund wechselt.

Android-Smartphone mit einer Alarm-Benachrichtigung aus einer Android-MQTT-Kotlin-App, die mit einem Cloud-Broker verbunden ist
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Einen Android MQTT Kotlin Client zu bauen ist leicht begonnen und erstaunlich leicht falsch gemacht: Die Verbindung muss Netzwerkwechsel überstehen, den App-Lebenszyklus respektieren und darf den Akku nicht leersaugen. In diesem Tutorial entwickeln Sie eine Kotlin-App, die sich per TLS mit MQTT 5 verbindet, Alarm-Topics abonniert, Android-Benachrichtigungen auslöst und sich lebenszyklusbewusst neu verbindet. Sie läuft zunächst gegen den öffentlichen MQTT-Broker broker.connectmq.com und dann gegen einen privaten ConnectMQ-Broker.

Eine Android-MQTT-Bibliothek wählen

Die Wahl der Bibliothek ist unter Android wichtiger als anderswo, weil viele alte Tutorials auf Code verweisen, der auf modernen Android-Versionen nicht mehr funktioniert.

Bibliothek Status für neue Android-Apps Hinweise
Eclipse Paho Android Service Nicht empfohlen Nicht mehr gepflegt; bekannte Probleme mit neueren Android-Versionen (Einschränkungen für Hintergrunddienste, PendingIntent-Flags)
Eclipse Paho Java Client (mqttv3) Nutzbar Reiner Java-Client; Threading und Lebenszyklus verwalten Sie selbst; nur MQTT 3.1.1
HiveMQ MQTT Client Hier empfohlen Gepflegt, MQTT 5 und 3.1.1, asynchrone API, integrierter automatischer Reconnect mit Resubscribe

Dieser Leitfaden verwendet den HiveMQ MQTT Client, eine universelle Java-Bibliothek, die unter Android läuft. Die Dokumentation auf der GitHub-Seite des HiveMQ MQTT Client beschreibt die Android-spezifische Einrichtung.

Projekt-Setup

Legen Sie in Android Studio ein neues Projekt mit einer Empty Activity (Compose) an. Setzen Sie minSdk auf 26 oder höher: So können Sie Benachrichtigungskanäle ohne Versionsprüfungen anlegen und vermeiden zusätzlichen Desugaring-Aufwand für die Java-8-APIs der Bibliothek.

In app/build.gradle.kts:

android {
    defaultConfig {
        minSdk = 26
    }
    packaging {
        resources {
            // Netty, intern vom Client genutzt, liefert doppelte Metadatendateien mit
            excludes += setOf("META-INF/INDEX.LIST", "META-INF/io.netty.versions.properties")
        }
    }
}

dependencies {
    implementation("com.hivemq:hivemq-mqtt-client:1.3.3") // auf Maven Central die neueste 1.3.x prüfen
    implementation("androidx.lifecycle:lifecycle-process:2.8.7")
}

Ohne die packaging-Ausschlüsse scheitert der Build typischerweise mit dem Fehler „more than one file was found“. Fügen Sie in AndroidManifest.xml die Berechtigungen hinzu:

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

POST_NOTIFICATIONS ist ab Android 13 eine Laufzeitberechtigung; Sie fordern sie in der Activity an.

Die Android-MQTT-Kotlin-Client-Klasse

Legen Sie MqttRepository.kt an. Die Klasse besitzt einen einzigen Client für die gesamte App, stellt den Verbindungsstatus und den letzten Alarm als StateFlow bereit und erzeugt für jede Alarmnachricht eine Benachrichtigung.

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) {
        // Läuft auf dem Netzwerk-Thread des Clients: keine UI-Arbeit, nur threadsichere Aufrufe
        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()
    }
}

Punkte, die Debugging-Zeit sparen:

  • Ein Client pro App. Ein Client pro Bildschirm führt zu doppelten Verbindungen mit derselben Client ID, die sich in einer Schleife gegenseitig vom Broker werfen.
  • Eine stabile Client ID, gespeichert in SharedPreferences und einmal pro Installation erzeugt.
  • Automatischer Reconnect mit Backoff fängt Wechsel von WLAN zu Mobilfunk und Tunnelfahrten ab. Der HiveMQ-Client abonniert automatisch neu, wenn der Broker die Session nicht mehr hat, sodass Sie Ihre Subscription nach einem Reconnect nicht verlieren.
  • Der Message-Callback läuft nicht auf dem Main Thread. Einen StateFlow zu aktualisieren und eine Benachrichtigung zu senden ist dort sicher; Views anzufassen nicht.
  • Ein vom Nutzer ausgelöstes disconnect() startet keinen automatischen Reconnect – genau das, was Sie wollen, wenn die App in den Hintergrund wechselt.

Lebenszyklusbewusste Verbindung

Einen MQTT-Socket offen zu halten, während die App unsichtbar ist, belastet den Akku und wird vom System ohnehin gekappt. Koppeln Sie die Verbindung mit ProcessLifecycleOwner an den sichtbaren Lebenszyklus der App; er meldet, wann die gesamte App (nicht eine einzelne Activity) zwischen Vorder- und Hintergrund wechselt.

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()
        })
    }
}

Registrieren Sie die Klasse mit android:name=".AlarmApp" im <application>-Element. Bildschirmdrehungen und Wechsel zwischen Ihren eigenen Activities trennen die Verbindung nicht mehr, weil der Lebenszyklus auf Prozessebene erst stoppt, wenn die App als Ganzes den Bildschirm verlässt.

Die Oberfläche und die Benachrichtigungsberechtigung

Eine minimale Compose-Activity, die die Benachrichtigungsberechtigung anfordert und den Zustand anzeigt:

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)
                }
            }
        }
    }
}

Mit einem simulierten Gerät testen

Starten Sie die App, erlauben Sie Benachrichtigungen und veröffentlichen Sie dann im Terminal einen Alarm, wie es ein Gerät tun würde:

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"

Eine Heads-up-Benachrichtigung erscheint, und der Bildschirm zeigt den Alarm. Schalten Sie für einige Sekunden den Flugmodus ein: Der Status wechselt auf „Disconnected“ und zurück, sobald das Netz wieder da ist, und neue Alarme treffen ein, ohne dass Sie die App neu starten. Verwenden Sie statt a7k2 ein eigenes Topic-Präfix und denken Sie daran, dass der öffentliche MQTT-Broker für alle lesbar und nur für Tests gedacht ist. Weitere CLI-Tricks finden Sie in den Beispielen für mosquitto_pub und mosquitto_sub.

Die Alarm-Topics gestalten

Benachrichtigungen sind nur so gut wie die Nachrichten dahinter. Halten Sie Alarm-Payloads kurz und menschenlesbar oder senden Sie kompaktes JSON mit einem Schweregrad-Feld und bauen Sie den Text in der App zusammen. Veröffentlichen Sie Alarme mit QoS 1, damit ein kurzer Netzwerkaussetzer sie nicht verschluckt, und verzichten Sie bei einmaligen Ereignissen auf das Retain-Flag: Ein retained Alarm würde bei jedem Reconnect der App eine neue Benachrichtigung auslösen. Müssen Sie den aktuellen Alarmzustand auf dem Bildschirm anzeigen, veröffentlichen Sie ihn separat auf einem retained Topic wie .../pump3/alarm/active und nutzen das Benachrichtigungs-Topic nur für Zustandswechsel.

Typisch im Industrieumfeld ist eine Instandhaltungs-App, die Alarme aus der Fertigung empfängt. Folgen die Topics der ISA-95-Hierarchie, etwa acme/werk-muenchen/abfuellung/linie-2/pumpe-3/alarm, kann die App mit einem Wildcard-Filter wie acme/werk-muenchen/+/+/+/alarm gezielt nur die Alarme eines Werks abonnieren.

Hintergrundalarme: die richtige Architektur wählen

Da die App im Hintergrund die Verbindung trennt, benachrichtigt sie nur, solange sie geöffnet ist. Für Alarme bei geschlossener App wählen Sie einen dieser Ansätze:

Ansatz Funktionsweise Abwägung
Serverseitiger Subscriber plus Push (empfohlen) Ein Backend-Service abonniert die Alarm-Topics und versendet Push-Benachrichtigungen Am akkuschonendsten und zuverlässigsten; erfordert ein kleines Backend
Foreground Service Die App hält die MQTT-Verbindung in einem Service mit sichtbarer, dauerhafter Benachrichtigung Funktioniert ohne Backend, aber neuere Android-Versionen schränken Typen und Laufzeit von Foreground Services ein, und Nutzer sehen eine permanente Benachrichtigung
Periodische Prüfung mit WorkManager Nach Zeitplan kurz verbinden und den retained Alarmzustand lesen Einfach, aber um Minuten verzögert und abhängig von Akku-Optimierungen

Für ein .NET-Backend eignet sich das Muster des ASP.NET-Core-MQTT-Background-Service gut als serverseitiger Subscriber. Plattformübergreifende Alternativen zu nativem Kotlin behandeln Flutter MQTT und React Native MQTT.

Fehlerbehebung

  • Build-Fehler wegen doppelter META-INF-Dateien: Fügen Sie die oben gezeigten packaging-Ausschlüsse hinzu.
  • SSLHandshakeException: falsche Geräteuhrzeit oder ein sehr alter Android-Truststore; der Broker nutzt ein Let's-Encrypt-Zertifikat.
  • Verbindet und trennt sich in einer Schleife: Ein anderer Client (oft ein Emulator und ein Smartphone) nutzt dieselbe Client ID.
  • Keine Benachrichtigung unter Android 13+: Der Nutzer hat POST_NOTIFICATIONS verweigert, oder der Kanal wurde in den Systemeinstellungen stummgeschaltet.
  • Not authorized auf einem privaten Broker: Die Client ID muss die mit Ihren Zugangsdaten ausgestellte sein.

Produktiv-Setup mit einem privaten Broker

Für echte Geräte und Nutzer verbinden Sie sich mit einem privaten, authentifizierten Broker. ConnectMQ generiert beim Anlegen eines Brokers Benutzername, Passwort und Client ID und bindet die Client ID an diese Zugangsdaten. Tauschen Sie die Builder-Zeilen aus:

MqttClient.builder()
    .useMqttVersion5()
    .identifier("YOUR_CLIENT_ID")
    .serverHost("YOUR_BROKER_HOST")
    .serverPort(8883)
    .sslWithDefaultConfig()
    .simpleAuth()
        .username("YOUR_USERNAME")
        .password("YOUR_PASSWORD".toByteArray())
        .applySimpleAuth()
    // ... Reconnect und Listener wie zuvor
    .buildAsync()

Codieren Sie keine geteilten Zugangsdaten fest in eine veröffentlichte APK; stellen Sie Zugangsdaten pro Nutzer oder pro Installation über Ihr Backend bereit und speichern Sie sie mit dem Android Keystore. MQTT-Sicherheit: Best Practices behandelt das ausführlich. Der kostenlose öffentliche MQTT-Broker ist der richtige Ort fürs Prototyping, ein verwalteter MQTT-Broker der richtige Ort für Produktivverkehr. Bibliotheksoptionen für andere Plattformen finden Sie im Leitfaden zu MQTT-Client-Bibliotheken.

Starten Sie mit ConnectMQ

Erstellen Sie Ihren privaten Broker unter app.connectmq.com im kostenlosen Serverless-Plan, ganz ohne Kreditkarte. Tragen Sie die generierten Zugangsdaten in den Builder ein, veröffentlichen Sie dann mit dem MQTT-Client im Browser des Portals Testalarme an Ihr Smartphone und behalten Sie Gerätewerte mit den Schwellenwert-Alarmregeln und dem Alarmprotokoll des Portals im Blick.

Häufig gestellte Fragen

Ist Eclipse Paho noch eine gute Wahl für Android?

Der Paho Android Service wird nicht mehr gepflegt und hat Probleme mit aktuellen Android-Versionen; meiden Sie ihn für neue Apps. Der reine Paho-Java-Client funktioniert, unterstützt aber nur MQTT 3.1.1; der HiveMQ MQTT Client ist für Kotlin die vollständigere Option.

Kann eine Android-App eine MQTT-Verbindung im Hintergrund offen halten?

Nur mit einem Foreground Service und einer sichtbaren Benachrichtigung, und neuere Android-Versionen schränken das weiter ein. Für Hintergrundalarme ist ein serverseitiger Subscriber, der Push-Benachrichtigungen versendet, zuverlässiger.

Unterstützt der HiveMQ MQTT Client Kotlin-Coroutines?

Er bietet asynchrone (CompletableFuture), reaktive und blockierende APIs. Die asynchrone API können Sie aus Coroutines mit der Erweiterung await() aus kotlinx-coroutines-jdk8 aufrufen.

Welchen Port sollte ein Android MQTT Client verwenden?

Verwenden Sie TLS auf Port 8883. Lässt ein Netzwerk nur HTTPS zu, nutzen Sie sicheres WebSocket auf Port 8084 mit dem Pfad /mqtt, was der HiveMQ-Client über seine WebSocket-Konfiguration unterstützt.

Verwandte Artikel

Die besten MQTT-CLI-Tools zum Testen, Skripten und Debuggen

Ein praxisnaher Vergleich von mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, der MQTT.js-CLI, curl und mqttui – mit demselben Test gegen broker.connectmq.com, damit Sie das richtige Tool zum Skripten und Debuggen wählen.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.