Tutoriais de IoT

Cliente MQTT para Android em Kotlin: Conecte, Assine e Notifique

Crie um app Android em Kotlin que se conecta a um broker MQTT via TLS, assina topics de alarme, dispara notificações do sistema e reconecta corretamente quando o app alterna entre primeiro e segundo plano.

Celular Android exibindo uma notificação de alarme de um app MQTT em Kotlin conectado a um broker na nuvem
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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 SharedPreferences e 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 StateFlow e 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 packaging mostradas 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_NOTIFICATIONS ou 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.

Artigos relacionados

Melhores Ferramentas CLI MQTT para Testes, Scripts e Depuração

Uma comparação prática entre mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, a CLI do MQTT.js, curl e mqttui, com o mesmo teste rodando no broker.connectmq.com, para você escolher a ferramenta certa para scripts e depuração.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.