IoT Eğitimleri

Android Kotlin MQTT Client: Bağlanma, Subscribe ve Bildirim

Bir MQTT broker'a TLS üzerinden bağlanan, alarm topic'lerine subscribe olan, sistem bildirimleri gösteren ve uygulama ön plan ile arka plan arasında geçiş yaptıkça doğru şekilde yeniden bağlanan bir Kotlin Android uygulaması geliştirin.

Bulut broker'a bağlı bir android kotlin mqtt uygulamasından gelen alarm bildirimini gösteren Android telefon
Bunu kendi MQTT broker’ınızda çalıştırınFree planla özel, yönetilen bir MQTT broker alın ve hazır kod örnekleriyle başlayın.
Ücretsiz deneyin Kod örnekleri Kart gerekmez

Android'de Kotlin ile bir MQTT client geliştirmeye başlamak kolaydır, ama şaşırtıcı derecede kolay biçimde yanlış da yapılabilir: bağlantının ağ değişikliklerinden sağ çıkması, uygulama yaşam döngüsüne uyması ve pili tüketmemesi gerekir. Bu eğitimde MQTT 5 ile TLS üzerinden bağlanan, alarm topic'lerine subscribe olan, Android bildirimleri gösteren ve yaşam döngüsüne duyarlı şekilde yeniden bağlanan bir Kotlin uygulaması geliştireceksiniz. Uygulama önce public broker broker.connectmq.com, ardından özel bir ConnectMQ broker'ı üzerinde çalışır.

Android MQTT Kütüphanesi Seçimi

Kütüphane seçimi Android'de her yerden daha önemlidir, çünkü pek çok eski eğitim modern Android sürümlerinde artık çalışmayan koda yönlendirir.

Kütüphane Yeni Android uygulamaları için durum Notlar
Eclipse Paho Android Service Önerilmez Bakımı yapılmıyor; yeni Android sürümlerinde bilinen sorunları var (arka plan servis kısıtlamaları, PendingIntent bayrakları)
Eclipse Paho Java client (mqttv3) Kullanılabilir Düz Java client; thread yönetimi ve yaşam döngüsü size kalır; yalnızca MQTT 3.1.1
HiveMQ MQTT Client Burada önerilen Bakımı yapılıyor, MQTT 5 ve 3.1.1, async API, yeniden subscribe ile yerleşik otomatik yeniden bağlanma

Bu rehber, Android üzerinde çalışan genel amaçlı bir Java kütüphanesi olan HiveMQ MQTT Client'ı kullanır. HiveMQ MQTT Client GitHub sayfasındaki dokümantasyon Android'e özgü kurulumu listeler.

Proje Kurulumu

Android Studio'da Empty Activity (Compose) ile yeni bir proje oluşturun. minSdk değerini 26 veya üzeri yapın: böylece sürüm kontrolü olmadan bildirim kanalları oluşturabilir ve kütüphanenin Java 8 API'leri için ek desugaring işinden kaçınırsınız.

app/build.gradle.kts içinde:

android {
    defaultConfig {
        minSdk = 26
    }
    packaging {
        resources {
            // Client'ın dahili olarak kullandığı Netty, yinelenen metadata dosyaları içerir
            excludes += setOf("META-INF/INDEX.LIST", "META-INF/io.netty.versions.properties")
        }
    }
}

dependencies {
    implementation("com.hivemq:hivemq-mqtt-client:1.3.3") // en güncel 1.3.x için Maven Central'a bakın
    implementation("androidx.lifecycle:lifecycle-process:2.8.7")
}

packaging hariç tutmaları olmadan derleme genellikle "more than one file was found" hatasıyla başarısız olur. AndroidManifest.xml dosyasına izinleri ekleyin:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

POST_NOTIFICATIONS, Android 13 ve sonrasında bir çalışma zamanı iznidir; bunu activity'den isteyeceksiniz.

Android Kotlin MQTT Client Sınıfı

MqttRepository.kt dosyasını oluşturun. Bu sınıf tüm uygulama için tek bir client'a sahiptir, bağlantı durumunu ve son alarmı StateFlow olarak sunar ve her alarm mesajı için bir bildirim gönderir.

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) {
        // Client'ın ağ thread'inde çalışır: burada UI işi yok, yalnızca thread-safe çağrılar
        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()
    }
}

Hata ayıklama süresinden tasarruf ettiren noktalar:

  • Uygulama başına tek client. Her ekran için bir client oluşturmak, aynı client ID'ye sahip yinelenen bağlantılara yol açar; bunlar birbirlerini döngü halinde broker'dan atar.
  • Sabit bir client ID, SharedPreferences içinde saklanır ve kurulum başına bir kez üretilir.
  • Backoff ile otomatik yeniden bağlanma, Wi-Fi'den mobil veriye geçişleri ve tünelleri yönetir. HiveMQ client, broker'da oturum artık yoksa otomatik olarak yeniden subscribe olur; böylece yeniden bağlandıktan sonra subscription'ınızı kaybetmezsiniz.
  • Mesaj callback'i ana thread dışında çalışır. Orada bir StateFlow güncellemek ve bildirim göndermek güvenlidir; view'lara dokunmak ise değildir.
  • Kullanıcının başlattığı disconnect() otomatik yeniden bağlanmayı tetiklemez; uygulama arka plana geçtiğinde istediğiniz de tam olarak budur.

Yaşam Döngüsüne Duyarlı Bağlantı

Uygulama görünmezken bir MQTT soketini açık tutmak pili tüketir ve zaten sistem tarafından kesilir. Bağlantıyı, tek bir activity'nin değil tüm uygulamanın ön plan ile arka plan arasında geçişini bildiren ProcessLifecycleOwner ile uygulamanın görünür yaşam döngüsüne bağlayın.

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

Bunu <application> öğesinde android:name=".AlarmApp" ile kaydedin. Ekran döndürme ve kendi activity'leriniz arasında geçiş artık bağlantıyı kesmez, çünkü süreç seviyesindeki yaşam döngüsü yalnızca uygulamanın tamamı ekrandan çıktığında durur.

Arayüz ve Bildirim İzni

Bildirim izni isteyen ve durumu gösteren minimal bir Compose activity:

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)
                }
            }
        }
    }
}

Simüle Edilmiş Bir Cihazla Test Edin

Uygulamayı çalıştırın, bildirimlere izin verin, ardından bir terminalden bir cihazın yapacağı gibi alarm publish edin:

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"

Bir heads-up bildirimi görünür ve ekran alarmı gösterir. Uçak modunu birkaç saniyeliğine açıp kapatın: durum "Disconnected" olur, ağ geri geldiğinde eski haline döner ve yeni alarmlar uygulamayı yeniden başlatmadan gelir. a7k2 yerine kendi topic önekinizi kullanın ve public alanın herkes tarafından okunabildiğini unutmayın. Daha fazla CLI ipucu mosquitto_pub ve mosquitto_sub örneklerinde bulunur.

Alarm Topic'lerini Tasarlama

Bildirimler ancak arkalarındaki mesajlar kadar iyidir. Alarm payload'larını kısa ve insan tarafından okunabilir tutun ya da severity alanı içeren kompakt bir JSON gönderip metni uygulamada oluşturun. Kısa bir ağ aksaklığının alarmları yutmaması için alarmları QoS 1 ile publish edin ve tek seferlik olaylar için retain bayrağından kaçının: retained bir alarm, uygulama her yeniden bağlandığında yeni bir bildirim tetikler. Ekranda güncel alarm durumunu göstermeniz gerekiyorsa bunu .../pump3/alarm/active gibi retained bir topic'te ayrıca publish edin ve bildirim topic'ini yalnızca durum geçişleri için kullanın.

Arka Plan Uyarıları: Doğru Mimariyi Seçin

Uygulama arka planda bağlantıyı kestiği için yalnızca açıkken bildirim gösterir. Uygulama kapalıyken uyarı almak için şunlardan birini seçin:

Yaklaşım Nasıl çalışır Ödünleşim
Sunucu tarafı subscriber ve push (önerilen) Bir backend servisi alarm topic'lerine subscribe olur ve push bildirimleri gönderir Pil açısından en verimli ve en güvenilir yöntem; küçük bir backend gerektirir
Foreground service Uygulama, MQTT bağlantısını görünür ve sürekli bir bildirime sahip bir serviste tutar Backend olmadan çalışır, ancak yeni Android sürümleri foreground service türlerini ve sürelerini kısıtlar; kullanıcılar kalıcı bir bildirim görür
Periyodik WorkManager kontrolü Bir zamanlamaya göre kısa süreliğine bağlanıp retained alarm durumunu okur Basittir, ancak dakikalarca gecikebilir ve pil optimizasyonlarına tabidir

.NET backend için ASP.NET Core MQTT arka plan servisi deseni, sunucu tarafı subscriber için iyi bir seçenektir. Yerel Kotlin'e platformlar arası alternatifler Flutter MQTT ve React Native MQTT yazılarında ele alınır.

Sorun Giderme

  • Yinelenen META-INF dosyalarıyla ilgili derleme hatası: yukarıda gösterilen packaging hariç tutmalarını ekleyin.
  • SSLHandshakeException: cihaz saati yanlış veya Android güven deposu çok eski; broker bir Let's Encrypt sertifikası kullanır.
  • Bağlanıp döngü halinde kopuyor: başka bir client (çoğu zaman bir emülatör ve bir telefon) aynı client ID'yi kullanıyor.
  • Android 13+ cihazda bildirim yok: kullanıcı POST_NOTIFICATIONS iznini reddetmiş veya kanal sistem ayarlarında sessize alınmış.
  • Özel broker'da yetkisiz (not authorized) hatası: client ID, kimlik bilgilerinizle birlikte verilen ID olmalıdır.

Özel Broker ile Üretim Kurulumu

Gerçek cihazlar ve kullanıcılar için özel, kimlik doğrulamalı bir broker'a bağlanın. ConnectMQ bir broker oluşturduğunuzda kullanıcı adı, parola ve client ID üretir ve client ID'yi bu kimlik bilgilerine bağlar. Builder satırlarını değiştirin:

MqttClient.builder()
    .useMqttVersion5()
    .identifier("YOUR_CLIENT_ID")
    .serverHost("YOUR_BROKER_HOST")
    .serverPort(8883)
    .sslWithDefaultConfig()
    .simpleAuth()
        .username("YOUR_USERNAME")
        .password("YOUR_PASSWORD".toByteArray())
        .applySimpleAuth()
    // ... yeniden bağlanma ve listener'lar önceki gibi
    .buildAsync()

Yayınlanan bir APK'ya paylaşılan bir kimlik bilgisini gömmeyin; backend'inizden kullanıcı başına veya kurulum başına kimlik bilgileri sağlayın ve bunları Android Keystore ile saklayın. MQTT güvenliği en iyi uygulamaları bu konuyu derinlemesine ele alır. Prototipleme için doğru yer ücretsiz public MQTT broker, üretim trafiği için doğru yer ise barındırılan bir MQTT broker'dır. Diğer platformlardaki kütüphane seçenekleri için MQTT client kütüphaneleri rehberine bakın.

ConnectMQ ile Başlayın

app.connectmq.com üzerinde ücretsiz Serverless planıyla, kredi kartı gerekmeden özel broker'ınızı oluşturun. Üretilen kimlik bilgilerini builder'a ekleyin; ardından telefonunuza test alarmları göndermek için portalın tarayıcı içi MQTT client'ını, cihaz değerlerini takip etmek için de eşik tabanlı uyarı kurallarını ve uyarı günlüğünü kullanın.

Sık Sorulan Sorular

Eclipse Paho Android için hâlâ iyi bir seçim mi?

Paho Android Service'in bakımı yapılmıyor ve yeni Android sürümlerinde sorunları var; bu yüzden yeni uygulamalarda ondan kaçının. Düz Paho Java client çalışır ama yalnızca MQTT 3.1.1'i destekler; HiveMQ MQTT Client, Kotlin için daha eksiksiz bir seçenektir.

Bir Android uygulaması arka planda MQTT bağlantısını açık tutabilir mi?

Yalnızca bir foreground service ve görünür bir bildirimle; yeni Android sürümleri bunu daha da kısıtlıyor. Arka plan uyarıları için push bildirimleri gönderen sunucu tarafı bir subscriber daha güvenilirdir.

HiveMQ MQTT Client Kotlin coroutine'lerini destekliyor mu?

Async (CompletableFuture), reaktif ve bloklayan API'ler sunar. Async API'yi coroutine'lerden kotlinx-coroutines-jdk8 paketindeki await() extension'ı ile çağırabilirsiniz.

Android MQTT client hangi portu kullanmalı?

8883 portunda TLS kullanın. Bir ağ yalnızca HTTPS'e izin veriyorsa, HiveMQ client'ın WebSocket yapılandırmasıyla desteklediği /mqtt yoluna sahip 8084 portunda güvenli WebSocket kullanın.

İlgili yazılar

Test, Betik ve Hata Ayıklama için En İyi MQTT CLI Araçları

mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, MQTT.js CLI, curl ve mqttui'nin uygulamalı karşılaştırması; betik yazma ve hata ayıklama için doğru aracı seçebilmeniz için aynı test broker.connectmq.com üzerinde çalıştırıldı.

7 dk okuma

Tüm yazılar →

Özel MQTT broker’ınız birkaç tık uzağınızda

Hesabınızı oluşturun, broker kimlik bilgilerinizi alın ve ilk cihazınızı bağlayın — kurulacak, yamalanacak ya da ölçeklendirilecek sunucu yok.