Criar um cliente MQTT para Android em Kotlin é fácil de começar e surpreendentemente fácil de errar: a conexão precisa sobreviver a mudanças de rede, respeitar o ciclo de vida do app e não drenar a bateria. Neste tutorial você vai construir um app em Kotlin que se conecta via TLS com MQTT 5, assina topics de alarme, dispara notificações do Android e reconecta de forma consciente do ciclo de vida. Primeiro ele roda contra o servidor MQTT público broker.connectmq.com e depois contra um broker MQTT gerenciado privado da ConnectMQ.
Escolhendo uma biblioteca MQTT para Android
A escolha da biblioteca importa mais no Android do que em qualquer outro lugar, porque muitos tutoriais antigos apontam para código que não funciona mais nas versões modernas do Android.
| Biblioteca | Situação para novos apps Android | Observações |
|---|---|---|
| Eclipse Paho Android Service | Não recomendada | Sem manutenção; problemas conhecidos com versões mais novas do Android (limites de serviços em segundo plano, flags de PendingIntent) |
Eclipse Paho Java client (mqttv3) |
Utilizável | Cliente Java puro; você mesmo gerencia threads e ciclo de vida; somente MQTT 3.1.1 |
| HiveMQ MQTT Client | Recomendada aqui | Mantida, MQTT 5 e 3.1.1, API assíncrona, reconexão automática integrada com nova assinatura |
Este guia usa o HiveMQ MQTT Client, uma biblioteca Java de uso geral que roda no Android. A documentação na página do HiveMQ MQTT Client no GitHub traz a configuração específica para Android.
Configuração do projeto
Crie um novo projeto no Android Studio com uma Empty Activity (Compose). Defina minSdk como 26 ou mais: isso permite criar canais de notificação sem verificações de versão e evita trabalho extra de desugaring para as APIs Java 8 da biblioteca.
Em app/build.gradle.kts:
android {
defaultConfig {
minSdk = 26
}
packaging {
resources {
// O Netty, usado internamente pelo cliente, inclui arquivos de metadados duplicados
excludes += setOf("META-INF/INDEX.LIST", "META-INF/io.netty.versions.properties")
}
}
}
dependencies {
implementation("com.hivemq:hivemq-mqtt-client:1.3.3") // confira no Maven Central a versão 1.3.x mais recente
implementation("androidx.lifecycle:lifecycle-process:2.8.7")
}
Sem as exclusões em packaging, o build normalmente falha com um erro "more than one file was found". Em AndroidManifest.xml, adicione as permissões:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
POST_NOTIFICATIONS é uma permissão de tempo de execução no Android 13 e posteriores; você vai solicitá-la a partir da activity.
A classe do cliente MQTT para Android em Kotlin
Crie MqttRepository.kt. Ela é dona de um único cliente para o app inteiro, expõe o estado da conexão e o último alarme como StateFlow e publica uma notificação para cada mensagem de 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) {
// Roda na thread de rede do cliente: nada de trabalho de UI aqui, apenas chamadas thread-safe
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()
}
}
Pontos que economizam tempo de depuração:
- Um cliente por app. Criar um cliente por tela gera conexões duplicadas com o mesmo client ID, que derrubam umas às outras do broker em loop.
- Um client ID estável, guardado em
SharedPreferencese gerado uma única vez por instalação. - Reconexão automática com backoff lida com a troca de Wi-Fi para dados móveis e com túneis. O cliente HiveMQ refaz as assinaturas automaticamente se o broker não tiver mais a sessão, então você não perde a assinatura após uma reconexão.
- O callback de mensagens roda fora da thread principal. Atualizar um
StateFlowe publicar uma notificação é seguro ali; mexer em views, não. - Um
disconnect()iniciado pelo usuário não dispara a reconexão automática, exatamente o que você quer quando o app vai para o segundo plano.
Conexão consciente do ciclo de vida
Manter um socket MQTT aberto enquanto o app está invisível drena a bateria e, de qualquer forma, acaba cortado pelo sistema. Amarre a conexão ao ciclo de vida visível do app com ProcessLifecycleOwner, que informa quando o app inteiro (e não uma activity) passa entre primeiro e segundo plano.
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()
})
}
}
Registre-a com android:name=".AlarmApp" no elemento <application>. Girar a tela e alternar entre as suas próprias activities deixam de desconectar, porque o ciclo de vida no nível do processo só para quando o app como um todo sai da tela.
A UI e a permissão de notificação
Uma activity Compose mínima que pede a permissão de notificação e mostra o estado:
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)
}
}
}
}
}
Teste com um dispositivo simulado
Rode o app, permita as notificações e, de um terminal, publique um alarme como um dispositivo faria, por exemplo a bomba de uma estação de saneamento:
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"
Uma notificação heads-up aparece e a tela mostra o alarme. Ative o modo avião por alguns segundos: o status muda para "Disconnected" e volta quando a rede retorna, e novos alarmes chegam sem reiniciar o app. Use o seu próprio prefixo de topic em vez de a7k2 e lembre-se de que o servidor MQTT público pode ser lido por qualquer pessoa e serve apenas para testes. Mais truques de CLI estão em exemplos de mosquitto_pub e mosquitto_sub.
Projetando os topics de alarme
As notificações só são tão boas quanto as mensagens por trás delas. Mantenha os payloads de alarme curtos e legíveis por humanos, ou envie um JSON compacto com um campo de severidade e monte o texto no app. Publique alarmes com QoS 1 para que uma breve oscilação da rede não os engula, e evite a flag retain para eventos pontuais: um alarme retido dispararia uma nova notificação toda vez que o app reconectasse. Se precisar mostrar na tela o estado atual dos alarmes, publique-o separadamente em um topic retido como .../pump3/alarm/active e reserve o topic de notificação apenas para transições.
Alertas em segundo plano: escolha a arquitetura certa
Como o app se desconecta em segundo plano, ele só notifica enquanto está aberto. Para alertas com o app fechado, escolha uma destas opções:
| Abordagem | Como funciona | Contrapartida |
|---|---|---|
| Assinante no servidor mais push (recomendado) | Um serviço backend assina os topics de alarme e envia push notifications | Mais econômico em bateria e mais confiável; exige um pequeno backend |
| Foreground service | O app mantém a conexão MQTT em um serviço com uma notificação contínua visível | Funciona sem backend, mas as versões mais novas do Android restringem tipos e duração de foreground services, e o usuário vê uma notificação permanente |
| Verificação periódica com WorkManager | Conecta brevemente em intervalos e lê o estado de alarme retido | Simples, mas com atraso de minutos e sujeita às otimizações de bateria |
Para um backend .NET, o padrão de serviço em segundo plano MQTT com ASP.NET Core é uma boa escolha para o assinante no servidor. Alternativas multiplataforma ao Kotlin nativo são abordadas em MQTT com Flutter e MQTT com React Native.
Solução de problemas
- Erro de build sobre arquivos META-INF duplicados: adicione as exclusões em
packagingmostradas acima. SSLHandshakeException: relógio do aparelho errado ou um repositório de certificados muito antigo no Android; o broker usa um certificado Let's Encrypt.- Conecta e desconecta em loop: outro cliente (muitas vezes um emulador e um celular) usa o mesmo client ID.
- Nenhuma notificação no Android 13+: o usuário negou
POST_NOTIFICATIONSou o canal foi silenciado nas configurações do sistema. - Não autorizado em um broker privado: o client ID precisa ser o emitido junto com as suas credenciais.
Configuração de produção com um broker privado
Para dispositivos e usuários reais, conecte-se a um broker privado e autenticado. O ConnectMQ gera usuário, senha e client ID quando você cria um broker e vincula o client ID a essas credenciais. Troque as linhas do builder:
MqttClient.builder()
.useMqttVersion5()
.identifier("YOUR_CLIENT_ID")
.serverHost("YOUR_BROKER_HOST")
.serverPort(8883)
.sslWithDefaultConfig()
.simpleAuth()
.username("YOUR_USERNAME")
.password("YOUR_PASSWORD".toByteArray())
.applySimpleAuth()
// ... reconexão e listeners como antes
.buildAsync()
Não coloque uma credencial compartilhada fixa no código de um APK publicado; provisione credenciais por usuário ou por instalação a partir do seu backend e guarde-as com o Android Keystore. Boas práticas de segurança MQTT aborda isso em detalhes. O servidor MQTT público (gratuito) é o lugar certo para prototipar, e um broker MQTT gerenciado é o lugar certo para o tráfego de produção. Para opções de bibliotecas em outras plataformas, veja o guia de bibliotecas cliente MQTT.
Comece a usar o ConnectMQ
Crie o seu broker privado em app.connectmq.com no plano gratuito Serverless, sem cartão de crédito. Coloque as credenciais geradas no builder e use o cliente MQTT no navegador do portal para publicar alarmes de teste no seu celular, além das regras de alerta por limite e do log de alertas para acompanhar os valores dos dispositivos.
Perguntas frequentes
O Eclipse Paho ainda é uma boa escolha para Android?
O Paho Android Service está sem manutenção e tem problemas nas versões recentes do Android, então evite-o em apps novos. O cliente Java Paho puro funciona, mas suporta apenas MQTT 3.1.1; o HiveMQ MQTT Client é uma opção mais completa para Kotlin.
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 as versões mais novas do Android restringem isso ainda mais. Para alertas em segundo plano, um assinante no servidor que envia push notifications é mais confiável.
O HiveMQ MQTT Client suporta coroutines do Kotlin?
Ele expõe APIs assíncrona (CompletableFuture), reativa e bloqueante. Você pode chamar a API assíncrona a partir de coroutines com a extensão await() do kotlinx-coroutines-jdk8.
Qual porta um cliente MQTT para Android deve usar?
Use TLS na porta 8883. Se uma rede só permitir HTTPS, use WebSocket seguro na porta 8084 com o caminho /mqtt, que o cliente HiveMQ suporta por meio da configuração de WebSocket.



