We encountered a situation: a customer connected 200 temperature and humidity sensors in a greenhouse. Data streams via MQTT every 2 seconds. A standard RecyclerView dashboard lagged: UI updated at 20 FPS, battery drained in 4 hours, and charts displayed with a 10-second delay. We had to redesign the architecture from scratch.
Building an IoT telemetry dashboard for a mobile app is more than just outputting a table. It aggregates realtime data from dozens of sensors, displays metrics widgets (numeric, trend charts, gauges) and device statuses. The key challenge is UI performance under frequent updates. Without proper architecture, you get UI lags, dropped packets, and high energy consumption. Our approach reduced UI load by 60% and increased battery life to 12 hours. Average server resource savings reach 40%, which for 1000 devices yields significant cost reduction. Server cost savings can reach 50%.
In this article, we'll break down how to build an IoT telemetry dashboard on Android (Kotlin, Jetpack Compose) using reactive streams for high performance and low power consumption. We use a combination of MQTT, Room, and StateFlow for realtime updates without loss.
How We Build the Dashboard: From MQTT to Widgets
The dashboard receives data from three sources: MQTT topics for realtime telemetry, REST API for historical data, and WebSocket for events (online/offline). All are merged into a single ViewModel using reactive streams.
On Android we use combine of several StateFlows:
class DashboardViewModel : ViewModel() { private val temperatureFlow = mqttRepository.getTopicFlow("sensors/+/temperature") private val humidityFlow = mqttRepository.getTopicFlow("sensors/+/humidity") private val devicesFlow = deviceRepository.devices val dashboardState = combine( temperatureFlow, humidityFlow, devicesFlow ) { temperatures, humidities, devices -> DashboardState( sensors = devices.map { device -> SensorWidgetData( id = device.id, name = device.name, temperature = temperatures[device.id], humidity = humidities[device.id], isOnline = device.isOnline ) } ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DashboardState()) } SharingStarted.WhileSubscribed(5_000) — the stream stops 5 seconds after the screen goes to background. This saves network connections and battery. On return, data is reloaded fresh.
Which Widgets to Use for an IoT Dashboard?
A typical dashboard contains several types of metric widgets, each with its own update pattern:
| Widget Type | Data | Update Frequency | Rendering |
|---|---|---|---|
| Numeric | Current value + status | 1 time/sec | Simple text with icon |
| Line chart (trend) | Trend for last hour | 1 time/min | Canvas |
| Gauge | Parameters with boundaries | 1 time/sec | Animated scale |
| Device card | Status + latest data | On event | Compose Card |
On Android Compose, each widget is a separate @Composable with a device key. The grid is implemented via LazyVerticalGrid:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 160.dp), contentPadding = PaddingValues(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { items(dashboardState.sensors, key = { it.id }) { sensor -> SensorWidget( sensor = sensor, modifier = Modifier.animateItemPlacement() ) } } animateItemPlacement() provides smooth animation when adding/removing widgets. Without key, Compose redraws the entire list on every update — critical for 200+ sensors.
Why StateFlow is Better than LiveData for Dashboards
StateFlow works natively with Jetpack Compose through collectAsState(), requiring no extra transformations. Unlike LiveData, StateFlow supports combine, flatMapLatest, and other coroutine operators. For a dashboard with multiple data sources, this reduces boilerplate and simplifies testing. Additionally, stateIn with WhileSubscribed gives fine-grained lifecycle control.
How to Configure Throttling for Battery Savings
Numeric widgets and frequent updates: MQTT can send data every second. Updating UI at that rate kills battery. Apply throttling at the Flow level:
temperatureFlow .throttleLatest(1000) // not more often than once per second .collect { updateWidget(it) } throttleLatest unlike debounce shows the latest value within the interval, not waiting for a pause. Choose throttleLatest if timeliness matters, not smoothing.
| Operator | Behavior | When to Use |
|---|---|---|
| throttleLatest | Takes the latest value per interval | Realtime metrics where missing a value is unacceptable |
| debounce | Waits for a pause after the last value | Search, text input |
throttleLatest reduces UI update frequency by 5 times when messages arrive once per second, yielding battery savings up to 30%.
How Historical Data is Cached
When the dashboard opens, historical data is needed for mini trend charts. Instead of loading everything at once, we load history lazily — only when the widget enters the viewport. Use LaunchedEffect:
@Composable fun SensorWidget(sensor: SensorWidgetData, viewModel: DashboardViewModel) { LaunchedEffect(sensor.id) { viewModel.loadHistory(sensor.id, hours = 1) } // Show skeleton while data loads val history by viewModel.getHistoryFlow(sensor.id).collectAsState(emptyList()) MiniChart(data = history) } Caching: store history in Room with TTL. Data older than 5 minutes is refetched from the server; fresh data is served from cache without a network request. This reduces server load by 40% and speeds up display.
User-Configurable Widgets
Drag-and-drop widgets, adding/removing sensors — optional but popular. Widget customization is done via an interface with compose-reorderable. The widget order is saved in DataStore. Users can choose which metrics appear on the main screen.
How We Develop the Dashboard: Stages
- Data source analysis — identify MQTT topics, REST endpoints, format, and message frequency.
- Reactive stream design — create ViewModel with combined StateFlows and throttling.
- Widget and grid layout — implement each widget type as a separate Composable with key.
- Caching integration — set up Room with TTL and lazy loading.
- Performance testing — measure FPS, battery drain, traffic volume.
- Optimization — apply throttleLatest, WhileSubscribed, animations via animateItemPlacement.
- Backend integration — connect REST, GraphQL, or WebSocket.
- Documentation and source code handover — full architecture and API docs.
What Is Included in the Work
- Dashboard architecture: reactive streams, ViewModel, DI (Hilt)
- Realtime protocol integration (MQTT, WebSocket)
- Widget and grid layout (Jetpack Compose / SwiftUI)
- History caching in Room / CoreData
- Drag-and-drop configuration and layout persistence
- Integration with your backend (REST, GraphQL)
- Performance testing on real devices
- Battery and traffic optimization
- Documentation and source code handover
Timelines and Cost
Development timeline for a typical dashboard: 4 to 6 weeks. Cost is calculated individually based on the number of widgets, data sources, and UI complexity. Contact us for a project assessment.
We have 5+ years of IoT experience and have delivered over 20 dashboards for industrial, agricultural, and smart home applications. Our engineers are certified in Android and iOS. We guarantee performance and post-delivery support.
If you are facing a similar challenge, get a consultation — we'll discuss your project and propose an optimal solution. Traffic savings of up to 40% and server cost reduction of 50% thanks to our caching and throttling approach. Discuss your project — contact today.







