Back to Blog
Mobile Development

Flutter + WebSocket: Building Real-Time IoT Dashboards That Don't Stutter

WebSocket integration in Flutter is deceptively hard to get right. Here's the production architecture we use across 10+ IoT apps — state management, reconnection logic, and smooth 60fps rendering of live sensor data.

February 8, 2024
10 min read
FlutterWebSocketIoTRiverpod

Flutter + WebSocket: Building Real-Time IoT Dashboards That Don't Stutter

When we built our first IoT dashboard in Flutter, we polled the API every second. It worked fine at demo scale. At production with 200 sensors, it hammered the server and the UI janked constantly.

WebSocket is the right answer. But getting it right in Flutter requires careful architecture.

The Architecture

IoT Devices → MQTT Broker → Node.js Backend
                                ↓ WebSocket
                          Flutter App
                         ↓           ↓
                  Riverpod         fl_chart
                  State         Real-time Charts

WebSocket Service

The WebSocket service is the heart of the real-time layer. It needs:

  • Auto-reconnection with exponential backoff
  • Message queue for offline periods
  • Stream-based API for reactive UI
  • class IoTWebSocketService {
      WebSocketChannel? _channel;
      final _controller = StreamController<DeviceData>.broadcast();
      Timer? _reconnectTimer;
      int _reconnectAttempts = 0;

    Stream<DeviceData> get dataStream => _controller.stream;

    void connect(String url) { try { _channel = WebSocketChannel.connect(Uri.parse(url)); _reconnectAttempts = 0;

    _channel!.stream.listen( _handleMessage, onError: _handleError, onDone: _scheduleReconnect, ); } catch (e) { _scheduleReconnect(); } }

    void _handleMessage(dynamic raw) { final json = jsonDecode(raw as String); final data = DeviceData.fromJson(json); _controller.add(data); }

    void _scheduleReconnect() { final delay = Duration( seconds: min(30, pow(2, _reconnectAttempts).toInt()), ); _reconnectAttempts++; _reconnectTimer = Timer(delay, () => connect(_url)); } }

    State Management with Riverpod

    Riverpod's StreamProvider is a perfect fit for WebSocket data:

    final iotDataProvider = StreamProvider.family<DeviceData, String>((ref, deviceId) {
      final service = ref.watch(wsServiceProvider);
      return service.dataStream.where((d) => d.deviceId == deviceId);
    });

    // In your widget: class SensorCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final data = ref.watch(iotDataProvider('device-001'));

    return data.when( data: (d) => SensorDisplay(data: d), loading: () => const SkeletonLoader(), error: (e, _) => const ErrorState(), ); } }

    60fps Charts Without Jank

    The key to smooth real-time charts is limiting the data window and using RepaintBoundary:

    class LiveChart extends StatefulWidget {
      final Stream<double> valueStream;
      final int windowSize; // show last N seconds

    // ... }

    class _LiveChartState extends State<LiveChart> { final _buffer = ListQueue<FlSpot>(); int _tick = 0;

    @override void initState() { super.initState(); widget.valueStream.listen((value) { setState(() { _buffer.add(FlSpot(_tick.toDouble(), value)); if (_buffer.length > widget.windowSize) _buffer.removeFirst(); _tick++; }); }); }

    @override Widget build(BuildContext context) { return RepaintBoundary( // Isolate chart repaints from parent child: LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: _buffer.toList(), isCurved: true, preventCurveOverShooting: true, color: const Color(0xFFFF6B35), dotData: const FlDotData(show: false), belowBarData: BarAreaData( show: true, color: const Color(0x22FF6B35), ), ), ], // Disable animations for real-time data lineTouchData: const LineTouchData(enabled: false), ), duration: Duration.zero, ), ); } }

    Offline-First with Hive

    IoT apps must work without network. Store the last known state:

    @HiveType(typeId: 0)
    class DeviceSnapshot extends HiveObject {
      @HiveField(0) String deviceId;
      @HiveField(1) double lastTemperature;
      @HiveField(2) DateTime lastSeen;
      @HiveField(3) bool isOnline;
    }

    // Cache on every update wsService.dataStream.listen((data) { final box = Hive.box<DeviceSnapshot>('devices'); box.put(data.deviceId, DeviceSnapshot.fromData(data)); });

    Performance Tips

  • 1. Use const constructors everywhere in list items
  • 2. ListView.builder with addRepaintBoundaries: true for device lists
  • 3. Debounce rapid updates — IoT data at 10Hz doesn't need 10 repaints/sec
  • 4. Profile with DevTools — identify widget rebuild hotspots
  • The difference between a smooth IoT dashboard and a janky one is usually in the state management layer. Get that right and the rest follows.

    Want us to build your IoT dashboard? [Let's talk](/contact).

    ⚡
    Written by CodeCaracal Engineering

    We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

    Let's Build Together

    Got an IoT challenge?
    We've shipped it.

    Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

    No upfront commitment99.9% uptime SLANDA on requestFixed-price options