Para usar MQTT em um app Android com Kotlin, adicione um cliente MQTT Java mantido — hoje o HiveMQ MQTT Client é o que melhor se encaixa —, encapsule-o em um repositório que exponha o estado da conexão e as mensagens como flows do Kotlin e vincule a conexão ao ciclo de vida do app em vez de tentar mantê-la viva para sempre. Este tutorial constrói exatamente isso, explica o que o Android faz com sockets de longa duração em segundo plano e mostra como testar o resultado contra um broker real.
Escolhendo a biblioteca cliente
| Biblioteca | Status no Android | Observações |
|---|---|---|
| HiveMQ MQTT Client | Funciona, manutenção ativa | MQTT 3.1.1 e 5; a API assíncrona retorna CompletableFuture, fácil de integrar com coroutines |
Eclipse Paho Java (mqttv3) | Funciona | Cliente MQTT 3.1.1 maduro com callbacks; você cuida das threads |
| Eclipse Paho Android Service | Evite | Sem manutenção, quebra em versões recentes do Android |
Os exemplos abaixo usam o cliente da HiveMQ. A mesma arquitetura funciona com o Paho Java — só mudam as chamadas de conexão e assinatura. Se você conhece o cliente da JVM, MQTT em Java com o HiveMQ MQTT Client cobre a API em detalhes.
Configuração do projeto
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" />O cliente depende de APIs do Java 8 como CompletableFuture, disponíveis a partir da API 24. Se o R8 reclamar de classes ausentes da dependência Netty do cliente em um build de release, adicione as regras keep da documentação Android do cliente.
Um repositório com coroutines e flows
Mantenha exatamente um cliente por app, pertencente a um objeto de longa duração — não a uma Activity, que é recriada a cada rotação. O repositório abaixo expõe o estado da conexão como StateFlow e as mensagens recebidas como SharedFlow:
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() vem de kotlinx.coroutines.future, que faz parte da biblioteca principal de coroutines, e transforma cada CompletableFuture em uma chamada suspensa. O callback da assinatura roda na thread do próprio cliente; tryEmit repassa a mensagem sem bloqueá-la. Um ViewModel então simplesmente coleta repository.messages e repository.state no viewModelScope.
Vincule a conexão ao ciclo de vida do app
O padrão confiável mais simples para a maioria dos apps: conectar quando o app vai para o primeiro plano e desconectar quando vai para o segundo plano. O ProcessLifecycleOwner reporta isso para o processo inteiro, não por Activity, então rotações e trocas de tela não causam reconexões:
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 um helper seu que retorna o UUID salvo por instalação. Não inclua credenciais reais do broker no BuildConfig de um app de produção — dê a cada usuário ou dispositivo suas próprias credenciais vindas do backend. Para testes rápidos, placeholders bastam.
O que o Android faz em segundo plano
Uma conexão TCP em um app em segundo plano não sobrevive por muito tempo, e isso é intencional:
- Doze e App Standby suspendem o acesso à rede em dispositivos ociosos e apps pouco usados. Seus PINGREQs de keep alive param, e o broker fecha a conexão após 1,5 × o intervalo de keep alive.
- Processos em cache podem ser congelados ou encerrados a qualquer momento para liberar memória; o socket morre junto.
- Economizadores de bateria dos fabricantes em muitos aparelhos são mais rígidos que o Android padrão e podem matar o trabalho em segundo plano de vez.
- Foreground services mantêm o processo rodando, mas exigem uma notificação visível e um tipo de serviço declarado no Android 14+, e o Android 15 limita por quanto tempo foreground services
dataSyncpodem rodar.
As consequências práticas: não prometa aos usuários entrega MQTT em tempo real com o app fechado. Se algo importante precisa chegar a um app fechado — um alarme, uma mensagem de chat —, envie uma push notification via Firebase Cloud Messaging a partir do backend e deixe que ela abra o app, que então se reconecta e sincroniza. Use um foreground service apenas quando o usuário estiver fazendo algo contínuo, como rastreamento ao vivo durante uma viagem. Desconexões do Android em segundo plano aprofunda o diagnóstico de cada caso.
Sessões persistentes: recuperando o atraso ao voltar
Quando o app volta ao primeiro plano, ele pode receber as mensagens QoS 1 que perdeu — se o broker manteve a sessão. Defina cleanStart(false) e um sessionExpiryInterval em connectWith() e mantenha o client ID estável. O broker então enfileira as mensagens das assinaturas enquanto o app está fora, até o tempo de expiração. Escolha um intervalo compatível com o tempo em que os dados perdidos continuam úteis; um dia inteiro de leituras de sensores enfileiradas chegando de uma vez raramente é o que a UI quer. O endpoint compartilhado de testes não mantém sessões offline, então teste isso com sua própria instância privada de broker. Leia sessões persistentes no MQTT para as regras exatas.
Mudanças de rede e keep alive
Celulares alternam o tempo todo entre Wi-Fi e dados móveis, e cada troca derruba silenciosamente as conexões TCP existentes. A reconexão automática do cliente cuida disso, mas a detecção é tão rápida quanto seu keep alive: com 60 segundos, uma conexão morta pode passar despercebida por até 90 segundos. Para reagir mais rápido, registre um ConnectivityManager.NetworkCallback e dispare uma reconexão em onAvailable. No celular, mantenha o intervalo entre 30 e 120 segundos — mais curto gasta bateria, mais longo atrasa a detecção. Keep alive no MQTT explica os tempos.
Teste em um dispositivo
- Rode o app em um dispositivo ou emulador e veja
statemudar paraConnected. - Abra o cliente MQTT online no computador, conecte-se ao mesmo broker e publique em
testmqtt/android/demo/hello. A mensagem deve aparecer no app. - Publique a partir do app e assine
testmqtt/android/#no navegador para confirmar a outra direção. - Coloque o app em segundo plano, espere mais que o timeout de keep alive e traga-o de volta. Você deve ver um novo estado
Connectede, com sessão persistente, as mensagens publicadas nesse meio-tempo. - Ative o modo avião por alguns segundos e confirme que o cliente se reconecta sozinho.
Veja as mensagens do seu app Android
Conectando do emulador a um broker no seu próprio computador? Use 10.0.2.2 em vez de localhost. Para mais verificações no dispositivo, veja testar um broker MQTT a partir do Android.
MQTT no Android funciona melhor quando você aceita as regras da plataforma: um cliente em um repositório de longa duração, uma conexão que segue o ciclo de vida do app, sessões persistentes para recuperar o atraso, push notifications para o que for urgente com o app fechado e TLS em todo lugar. Construa isso uma vez e cada tela do app simplesmente coleta um flow.
Perguntas frequentes
Ainda devo usar o Eclipse Paho Android Service?
Não em apps novos. A biblioteca org.eclipse.paho.android.service está sem manutenção há anos e quebra em versões recentes do Android (por exemplo, pelos requisitos de mutabilidade de PendingIntent desde o Android 12). Use o HiveMQ MQTT Client ou o cliente Paho Java comum dentro da arquitetura do seu app.
Um app Android consegue manter uma conexão MQTT aberta em segundo plano?
Só com um foreground service e uma notificação visível, e mesmo assim o Doze, os economizadores de bateria dos fabricantes e as novas regras de foreground services podem interrompê-la. Para a maioria dos apps, conecte enquanto o app está visível e use push notifications (FCM) para acordá-lo em eventos importantes.
Por que minha conexão MQTT funciona no Wi-Fi, mas não nos dados móveis?
Algumas redes móveis e Wi-Fi corporativos bloqueiam portas não HTTP como 1883 e 8883. Conecte via WebSockets seguros na porta 443 ou 8084 para testar se o problema é a porta, e use sempre TLS em redes que você não controla.


