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,
SharedPreferencesiç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
StateFlowgü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
packaginghariç 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_NOTIFICATIONSiznini 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.



