Tutoriais

MQTT no Android com Kotlin

Crie um cliente MQTT no Android com Kotlin: HiveMQ client com coroutines, opções do Paho, ciclo de vida, Doze e limites em background, TLS e como testar.

Atualizado · 9 min de leitura

MQTT no Android com Kotlin

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

BibliotecaStatus no AndroidObservações
HiveMQ MQTT ClientFunciona, manutenção ativaMQTT 3.1.1 e 5; a API assíncrona retorna CompletableFuture, fácil de integrar com coroutines
Eclipse Paho Java (mqttv3)FuncionaCliente MQTT 3.1.1 maduro com callbacks; você cuida das threads
Eclipse Paho Android ServiceEviteSem 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

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

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:

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

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 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 dataSync podem 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

  1. Rode o app em um dispositivo ou emulador e veja state mudar para Connected.
  2. 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.
  3. Publique a partir do app e assine testmqtt/android/# no navegador para confirmar a outra direção.
  4. 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 Connected e, com sessão persistente, as mensagens publicadas nesse meio-tempo.
  5. 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.