Um MQTT in einer Android-App mit Kotlin zu nutzen, fügst du einen gepflegten Java-MQTT-Client hinzu — der HiveMQ MQTT Client passt heute am besten —, kapselst ihn in einem Repository, das Verbindungsstatus und Nachrichten als Kotlin-Flows bereitstellt, und koppelst die Verbindung an den Lifecycle deiner App, statt sie für immer am Leben halten zu wollen. Dieses Tutorial baut genau das, erklärt, was Android mit langlebigen Sockets im Hintergrund macht, und zeigt, wie du das Ergebnis gegen einen echten Broker testest.
Die Client-Bibliothek wählen
| Bibliothek | Status unter Android | Hinweise |
|---|---|---|
| HiveMQ MQTT Client | Funktioniert, aktiv gepflegt | MQTT 3.1.1 und 5; die Async-API liefert CompletableFuture — leicht mit Coroutines zu verbinden |
Eclipse Paho Java (mqttv3) | Funktioniert | Ausgereifter MQTT-3.1.1-Client mit Callbacks; Threading übernimmst du selbst |
| Eclipse Paho Android Service | Vermeiden | Nicht gepflegt, bricht auf aktuellen Android-Versionen |
Die Beispiele nutzen den HiveMQ-Client. Dieselbe Architektur funktioniert mit Paho Java — nur die Connect- und Subscribe-Aufrufe unterscheiden sich. Wenn du den Client von der JVM kennst, behandelt MQTT in Java mit dem HiveMQ MQTT Client seine API im Detail.
Projekt-Setup
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")
}<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />Der Client nutzt Java-8-APIs wie CompletableFuture, die ab API 24 verfügbar sind. Meldet R8 beim Release-Build fehlende Klassen aus der Netty-Abhängigkeit des Clients, ergänze die Keep-Regeln aus der Android-Dokumentation des Clients.
Ein Repository mit Coroutines und Flows
Halte genau einen Client pro App, verwaltet von einem langlebigen Objekt — nicht von einer Activity, die bei jeder Rotation neu erstellt wird. Das folgende Repository stellt den Verbindungsstatus als StateFlow und eingehende Nachrichten als SharedFlow bereit:
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() stammt aus kotlinx.coroutines.future, das zur Core-Bibliothek von Coroutines gehört, und macht aus jedem CompletableFuture einen suspendierenden Aufruf. Der Subscription-Callback läuft auf dem eigenen Thread des Clients; tryEmit übergibt die Nachricht, ohne ihn zu blockieren. Ein ViewModel sammelt dann einfach repository.messages und repository.state im viewModelScope.
Die Verbindung an den App-Lifecycle koppeln
Das einfachste zuverlässige Muster für die meisten Apps: verbinden, wenn die App in den Vordergrund kommt, trennen, wenn sie in den Hintergrund geht. ProcessLifecycleOwner meldet das für den gesamten Prozess, nicht pro Activity — Rotationen und Bildschirmwechsel lösen also keine Reconnects aus:
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 steht für deinen eigenen Helper, der die gespeicherte UUID pro Installation zurückgibt. Liefere in einer Produktions-App keine echten Broker-Zugangsdaten in BuildConfig aus — gib jedem Nutzer oder Gerät eigene Zugangsdaten von deinem Backend. Für schnelle Tests reichen Platzhalter.
Was Android im Hintergrund macht
Eine TCP-Verbindung in einer App im Hintergrund überlebt nicht lange, und das ist so gewollt:
- Doze und App Standby setzen den Netzwerkzugriff für inaktive Geräte und selten genutzte Apps aus. Deine Keep-Alive-PINGREQs bleiben aus, und der Broker schließt die Verbindung nach dem 1,5-Fachen des Keep-Alive-Intervalls.
- Gecachte Prozesse können jederzeit eingefroren oder beendet werden, um Speicher freizugeben; der Socket stirbt mit ihnen.
- Akku-Sparmodi der Hersteller sind auf vielen Geräten strenger als Stock-Android und können Hintergrundarbeit komplett beenden.
- Foreground Services halten den Prozess am Laufen, verlangen aber eine sichtbare Benachrichtigung und ab Android 14 einen deklarierten Service-Typ, und Android 15 begrenzt, wie lange
dataSync-Foreground-Services laufen dürfen.
Die praktischen Folgen: Versprich Nutzern keine MQTT-Zustellung in Echtzeit bei geschlossener App. Muss etwas Wichtiges eine geschlossene App erreichen — ein Alarm, eine Chatnachricht —, sende von deinem Backend eine Push-Benachrichtigung über Firebase Cloud Messaging und lass sie die App öffnen, die sich dann neu verbindet und synchronisiert. Nutze einen Foreground Service nur, wenn der Nutzer gerade etwas Fortlaufendes tut, etwa Live-Tracking während einer Fahrt. Android-Verbindungsabbrüche im Hintergrund geht tiefer in die Diagnose jedes Falls.
Persistente Sessions: nach der Rückkehr aufholen
Wenn die App in den Vordergrund zurückkehrt, kann sie die verpassten QoS-1-Nachrichten empfangen — sofern der Broker ihre Session behalten hat. Setze cleanStart(false) und ein sessionExpiryInterval in connectWith() und halte die Client-ID stabil. Der Broker puffert dann Nachrichten für die Abonnements, solange die App weg ist, bis zum Ablauf der Expiry-Zeit. Wähle ein Intervall, das dazu passt, wie lange verpasste Daten nützlich bleiben; ein Tag gepufferter Sensorwerte, die auf einmal eintreffen, ist selten das, was die UI will. Der geteilte Test-Endpoint speichert keine Offline-Sessions, probiere das also mit deiner eigenen privaten Broker-Instanz aus. Die genauen Regeln stehen in MQTT Persistent Sessions.
Netzwechsel und Keep Alive
Smartphones wechseln ständig zwischen WLAN und Mobilfunk, und jeder Wechsel beendet bestehende TCP-Verbindungen stillschweigend. Der automatische Reconnect des Clients fängt das ab, aber die Erkennung ist nur so schnell wie dein Keep Alive: Bei 60 Sekunden kann eine tote Verbindung bis zu 90 Sekunden unbemerkt bleiben. Um schneller zu reagieren, registriere einen ConnectivityManager.NetworkCallback und stoße in onAvailable einen Reconnect an. Halte das Intervall auf Mobilgeräten zwischen 30 und 120 Sekunden — kürzer kostet Akku, länger verzögert die Erkennung. MQTT Keep Alive erklärt das Timing.
Auf einem Gerät testen
- Starte die App auf einem Gerät oder Emulator und beobachte, wie
statezuConnectedwechselt. - Öffne den Online-MQTT-Client auf deinem Computer, verbinde dich mit demselben Broker und publishe auf
testmqtt/android/demo/hello. Die Nachricht sollte in der App erscheinen. - Publishe aus der App und abonniere im Browser
testmqtt/android/#, um die andere Richtung zu bestätigen. - Schick die App in den Hintergrund, warte länger als den Keep-Alive-Timeout und hol sie zurück. Du solltest einen neuen
Connected-Status sehen und, mit persistenter Session, die zwischenzeitlich gepublishten Nachrichten. - Schalte den Flugmodus für ein paar Sekunden ein und prüfe, ob sich der Client von selbst neu verbindet.
Nachrichten deiner Android-App beobachten
Verbindest du dich vom Emulator mit einem Broker auf deinem eigenen Rechner? Nutze 10.0.2.2 statt localhost. Weitere Prüfungen vom Gerät aus findest du unter MQTT-Broker von Android aus testen.
MQTT unter Android funktioniert am besten, wenn du die Regeln der Plattform akzeptierst: ein Client in einem langlebigen Repository, eine Verbindung, die dem App-Lifecycle folgt, persistente Sessions zum Aufholen, Push-Benachrichtigungen für Dringendes bei geschlossener App und überall TLS. Baue das einmal, und jeder Screen deiner App sammelt einfach einen Flow.
Häufig gestellte Fragen
Sollte ich noch den Eclipse Paho Android Service verwenden?
Nicht für neue Apps. Die Bibliothek org.eclipse.paho.android.service wird seit Jahren nicht mehr gepflegt und funktioniert auf aktuellen Android-Versionen nicht mehr richtig (zum Beispiel wegen der PendingIntent-Mutability-Anforderungen seit Android 12). Nutze stattdessen den HiveMQ MQTT Client oder den normalen Paho-Java-Client in deiner eigenen App-Architektur.
Kann eine Android-App eine MQTT-Verbindung im Hintergrund offen halten?
Nur mit einem Foreground Service und einer sichtbaren Benachrichtigung, und selbst dann können Doze, Akku-Sparmodi der Hersteller und neuere Foreground-Service-Regeln sie unterbrechen. Für die meisten Apps gilt: verbinden, solange die App sichtbar ist, und Push-Benachrichtigungen (FCM) nutzen, um die App bei wichtigen Ereignissen aufzuwecken.
Warum funktioniert meine MQTT-Verbindung im WLAN, aber nicht im Mobilfunknetz?
Manche Mobilfunknetze und Firmen-WLANs blockieren Nicht-HTTP-Ports wie 1883 und 8883. Verbinde dich über sichere WebSockets auf Port 443 oder 8084, um zu prüfen, ob der Port das Problem ist, und nutze in fremden Netzen immer TLS.


