๐Ÿ“ฑ Phase 6

Android App Integration

Mobile app ke through remote device control aur live response loop implement karna

๐Ÿ“‹ Topics Covered in Phase 6

1Android MQTT client setup
2User login + device mapping flow
3Real-time relay control from app
4Full integration testing: device + dashboard + app

โš™๏ธ Step 1: Android Project Setup

๐Ÿ’ก

Required Tools:
โ€ข Android Studio (latest version)
โ€ข Kotlin knowledge basics
โ€ข Gradle 8.x
โ€ข Minimum SDK: 24 (Android 7.0)
โ€ข Target SDK: 34 (Android 14)

build.gradle (Module)

// app/build.gradle plugins { id('com.android.application') id('org.jetbrains.kotlin.android') } android { namespace = "com.example.iotmqtt" compileSdk = 34 defaultConfig { applicationId = "com.example.iotmqtt" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } } dependencies { // Android Core implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") // MQTT Client implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5") // Coroutines for async operations implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") // ViewModel and LiveData implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") // Navigation implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") implementation("androidx.navigation:navigation-ui-ktx:2.7.6") }

AndroidManifest.xml

<!-- AndroidManifest.xml --> <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Network permissions --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <!-- For foreground service (optional) --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:usesCleartextTraffic="true" <!-- For HTTP during development --> android:theme="@style/Theme.IoTMQTT"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

๐Ÿ”Œ Step 2: MQTT Service Class

// MqttService.kt - Centralized MQTT handling package com.example.iotmqtt import android.content.Context import org.eclipse.paho.client.mqttv3.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import java.util.* class MqttService(private val context: Context) { // Configuration private var mqttClient: MqttAsyncClient? = null private val serverUri = "tcp://broker.example.com:1883" // Change this private val clientId = "android-" + UUID.randomUUID().toString() // Connection state private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.DISCONNECTED) val connectionState: StateFlow<ConnectionState> = _connectionState // Messages received private val _messages = MutableStateFlow<Map<String, String>>(emptyMap()) val messages: StateFlow<Map<String, String>> = _messages // Callbacks for UI updates var onMessageReceived: ((topic: String, payload: String) -> Unit)? = null // =========================================== // Connection Management // =========================================== fun connect() { try { _connectionState.value = ConnectionState.CONNECTING val options = MqttConnectOptions().apply { isCleanSession = true connectionTimeout = 30 keepAliveInterval = 60 isAutomaticReconnect = true // isAutomaticReconnect requires paho 1.2+ } mqttClient = MqttAsyncClient(serverUri, clientId, null).apply { setCallback(object : MqttCallback { override fun connectionLost(cause: Throwable?) { _connectionState.value = ConnectionState.DISCONNECTED cause?.printStackTrace() } override fun messageArrived(topic: String, message: MqttMessage) { val payload = String(message.payload()) handleMessage(topic, payload) } override fun deliveryComplete(token: IMqttDeliveryToken?) { // Message delivered } }) connect(options, null, object : IMqttActionListener { override fun onSuccess(asyncActionToken: IMqttAsyncToken?) { _connectionState.value = ConnectionState.CONNECTED subscribeToTopics() } override fun onFailure(asyncActionToken: IMqttAsyncToken?, exception: Throwable?) { _connectionState.value = ConnectionState.ERROR exception?.printStackTrace() } }) } } catch (e: MqttException) { _connectionState.value = ConnectionState.ERROR e.printStackTrace() } } fun disconnect() { mqttClient?.disconnect() _connectionState.value = ConnectionState.DISCONNECTED } // =========================================== // Subscribe to Topics // =========================================== private fun subscribeToTopics() { val topics = listOf( "home/relay/+/status", "home/relay/+/telemetry", "home/relay/+/response" ) topics.forEach { topic -> mqttClient?.subscribe(topic, 1, null, object : IMqttActionListener { override fun onSuccess(asyncActionToken: IMqttAsyncToken?) { android.util.Log.d("MQTT", "Subscribed to: $topic") } override fun onFailure(asyncActionToken: IMqttAsyncToken?, exception: Throwable?) { android.util.Log.e("MQTT", "Subscribe failed: $topic") } }) } } // =========================================== // Publish Messages // =========================================== fun publish(topic: String, payload: String, qos: Int = 1) { try { val message = MqttMessage(payload.toByteArray()).apply { this.qos = qos } mqttClient?.publish(topic, message) } catch (e: MqttException) { e.printStackTrace() } } // =========================================== // Handle Incoming Messages // =========================================== private fun handleMessage(topic: String, payload: String) { // Update state _messages.value = _messages.value.toMutableMap().apply { put(topic, payload) } // Notify callback onMessageReceived?.invoke(topic, payload) } // =========================================== // Helper: Control Device // =========================================== fun turnOn(deviceId: String) { publish("home/relay/$deviceId/command", "ON") } fun turnOff(deviceId: String) { publish("home/relay/$deviceId/command", "OFF") } fun toggle(deviceId: String) { publish("home/relay/$deviceId/command", "TOGGLE") } fun requestStatus(deviceId: String) { publish("home/relay/$deviceId/command", "STATUS") } } // Connection state enum enum class ConnectionState { CONNECTED, CONNECTING, DISCONNECTED, ERROR }

๐Ÿ“ฑ Step 3: ViewModel for Device State

// MainViewModel.kt package com.example.iotmqtt import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class MainViewModel(application: Application) : AndroidViewModel(application) { // MQTT Service instance private val mqttService = MqttService(application) // UI State val connectionState = mqttService.connectionState val messages = mqttService.messages // Devices list private val _devices = MutableStateFlow<List<Device>>( listOf( Device("esp32-001", "Living Room Light", "Living Room", false, false), Device("esp32-002", "Bedroom Fan", "Bedroom", false, false), Device("esp32-003", "Kitchen Exhaust", "Kitchen", false, false) ) ) val devices: StateFlow<List<Device>> = _devices // Log entries private val _logs = MutableStateFlow<List<LogEntry>>(emptyList()) val logs: StateFlow<List<LogEntry>> = _logs init { // Set up message handler mqttService.onMessageReceived = this::handleMessage } // =========================================== // Connection Actions // =========================================== fun connect() { viewModelScope.launch { addLog("System", "Connecting to broker...") mqttService.connect() } } fun disconnect() { viewModelScope.launch { mqttService.disconnect() addLog("System", "Disconnected") } } // =========================================== // Device Control // =========================================== fun toggleDevice(deviceId: String) { viewModelScope.launch { mqttService.toggle(deviceId) addLog("Command", "Toggle sent to $deviceId") } } fun turnOn(deviceId: String) { viewModelScope.launch { mqttService.turnOn(deviceId) addLog("Command", "ON sent to $deviceId") } } fun turnOff(deviceId: String) { viewModelScope.launch { mqttService.turnOff(deviceId) addLog("Command", "OFF sent to $deviceId") } } // =========================================== // Message Handling // =========================================== private fun handleMessage(topic: String, payload: String) { viewModelScope.launch { addLog(topic, payload) // Parse topic val parts = topic.split("/") if (parts.size >= 4) { val deviceId = parts[2] // Update device state based on topic type _devices.value = _devices.value.map { device -> if (device.id == deviceId) { when { topic.contains("/status") -> { try { val json = org.json.JSONObject(payload) device.copy( isOnline = json.optString("state") == "online", isOn = json.optString("relay") == "on" ) } catch (e: Exception) { device } } else -> device } } else device } } } } // =========================================== // Logging // =========================================== private fun addLog(topic: String, message: String) { val entry = LogEntry( timestamp = System.currentTimeMillis(), topic = topic, message = message ) _logs.value = listOf(entry) + _logs.value.take(49) // Keep last 50 } override fun onCleared() { super.onCleared() mqttService.disconnect() } } // Data classes data class Device( val id: String, val name: String, val location: String, val isOn: Boolean, val isOnline: Boolean ) data class LogEntry( val timestamp: Long, val topic: String, val message: String )

๐ŸŽจ Step 4: MainActivity with UI

// MainActivity.kt package com.example.iotmqtt import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.example.iotmqtt.databinding.ActivityMainBinding import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.* class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val viewModel: MainViewModel by viewModels() private val deviceAdapter = DeviceAdapter { device, action -> when (action) { "toggle" -> viewModel.toggleDevice(device.id) "on" -> viewModel.turnOn(device.id) "off" -> viewModel.turnOff(device.id) } } private val logAdapter = LogAdapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setupUI() observeViewModel() } private fun setupUI() { // Setup RecyclerViews binding.recyclerDevices.apply { layoutManager = LinearLayoutManager(this@MainActivity) adapter = deviceAdapter } binding.recyclerLogs.apply { layoutManager = LinearLayoutManager(this@MainActivity) adapter = logAdapter } // Connect button binding.btnConnect.setOnClickListener { if (viewModel.connectionState.value == ConnectionState.CONNECTED) { viewModel.disconnect() } else { viewModel.connect() } } } private fun observeViewModel() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { // Connection state launch { viewModel.connectionState.collect { state -> when (state) { ConnectionState.CONNECTED -> { binding.btnConnect.text = "Disconnect" binding.tvConnectionStatus.text = "๐ŸŸข Connected" } ConnectionState.CONNECTING -> { binding.btnConnect.text = "Connecting..." binding.tvConnectionStatus.text = "๐ŸŸก Connecting" } ConnectionState.DISCONNECTED -> { binding.btnConnect.text = "Connect" binding.tvConnectionStatus.text = "โšซ Disconnected" } ConnectionState.ERROR -> { binding.btnConnect.text = "Retry" binding.tvConnectionStatus.text = "๐Ÿ”ด Error" } } } } // Devices launch { viewModel.devices.collect { devices -> deviceAdapter.submitList(devices) } } // Logs launch { viewModel.logs.collect { logs -> logAdapter.submitList(logs) } } } } } }
๐Ÿ’ก

Additional Files Needed:
โ€ข DeviceAdapter.kt - RecyclerView adapter for device cards
โ€ข LogAdapter.kt - RecyclerView adapter for log entries
โ€ข activity_main.xml - Layout with device list and log view
โ€ข colors.xml, themes.xml - Material Design styling

๐Ÿงช Step 5: Full Integration Testing

// =========================================== // Integration Test Checklist // =========================================== // TEST 1: ESP32 โ†’ Dashboard // 1. Power on ESP32 device // 2. Check Serial Monitor - device should connect to WiFi & MQTT // 3. Dashboard should show device as "Online" // 4. Toggle switch on dashboard โ†’ ESP32 relay should respond // TEST 2: Android App โ†’ ESP32 // 1. Open Android app // 2. Connect to MQTT broker // 3. Press ON button on device card // 4. ESP32 should receive command and toggle relay // TEST 3: Cross-platform sync // 1. Turn on device from Android app // 2. Check Dashboard - should show same state // 3. Turn off from Dashboard // 4. Check Android app - should update // TEST 4: Offline handling // 1. Disconnect ESP32 from power // 2. Dashboard should show "Offline" within 60 seconds // 3. Android app should show "Offline" // 4. Reconnect ESP32 // 5. Both should show "Online" again // =========================================== // Common Issues & Solutions // =========================================== // Issue: "Client already connected" // Solution: Check connection state before calling connect() // Issue: Messages not arriving on Android // Solution: Check QoS level matches (QoS 0, 1, or 2) // Issue: App crashes on background // Solution: Use Foreground Service for MQTT connection // Issue: Battery drain // Solution: Implement intelligent reconnection, not keep-alive