Back to Blog
Mobile Development

Flutter BLE: Building a Bluetooth IoT Controller App from Scratch

Most Flutter BLE tutorials show you how to scan for devices. This guide goes further — GATT services, characteristic notifications, real-time sensor streaming, and background BLE on both iOS and Android, with production-tested patterns.

August 12, 2024
13 min read
FlutterBLEBluetoothIoT

Flutter BLE: Building a Bluetooth IoT Controller App from Scratch

Bluetooth Low Energy is the right choice for short-range IoT devices — wearables, industrial sensors, home automation, and medical devices all use BLE because it consumes milliwatts instead of watts. Flutter's BLE ecosystem has matured significantly, and flutter_blue_plus is now the production-standard library.

This guide takes you from zero to a working Flutter BLE IoT controller that scans, connects, reads sensor data in real time, and writes commands to your embedded device.

Understanding BLE for IoT Engineers

Before touching Flutter code, you need to understand BLE's data model:

Device (Peripheral)
  └── Service (UUID: 180F = Battery, or custom UUID)
        ├── Characteristic (UUID: read/write/notify)
        │     └── Value (bytes)
        └── Characteristic (UUID: write-only)
              └── Value (bytes: your command)

Your ESP32/STM32 firmware exposes Services (logical groups) containing Characteristics (individual data points). A temperature sensor might expose:

  • Service UUID: 12345678-1234-1234-1234-1234567890AB
  • - Characteristic TEMP (UUID: ...01): read + notify, value is 4-byte float - Characteristic CTRL (UUID: ...02): write-only, accepts on/off command

    Setup: flutter_blue_plus

    pubspec.yaml

    dependencies: flutter_blue_plus: ^1.31.0 permission_handler: ^11.0.0

    iOS permissions (Info.plist)

    NSBluetoothAlwaysUsageDescription
    Required to connect to IoT sensors
    NSBluetoothPeripheralUsageDescription
    Required to connect to IoT sensors
    

    Android permissions (AndroidManifest.xml)

    
    
    
    

    Step 1: Scanning for BLE Devices

    import 'package:flutter_blue_plus/flutter_blue_plus.dart';

    class BLEScanService { StreamSubscription>? _scanSub;

    Stream> get scanResults => FlutterBluePlus.scanResults;

    Future startScan({Duration timeout = const Duration(seconds: 10)}) async { // Request permissions first await Permission.bluetoothScan.request(); await Permission.bluetoothConnect.request();

    await FlutterBluePlus.startScan( withServices: [Guid('12345678-1234-1234-1234-1234567890AB')], // Filter by your service UUID timeout: timeout, androidUsesFineLocation: false, ); }

    Future stopScan() async { await FlutterBluePlus.stopScan(); } }

    Filtering by service UUID is critical in production — it prevents your app from showing every BLE device nearby and only surfaces your IoT hardware.

    Step 2: Connecting and Discovering Services

    class BLEDeviceService {
      BluetoothDevice? _device;
      BluetoothCharacteristic? _tempCharacteristic;
      BluetoothCharacteristic? _ctrlCharacteristic;

    static const String SERVICE_UUID = '12345678-1234-1234-1234-1234567890AB'; static const String TEMP_UUID = '12345678-1234-1234-1234-1234567890AC'; static const String CTRL_UUID = '12345678-1234-1234-1234-1234567890AD';

    Future connect(BluetoothDevice device) async { _device = device;

    await device.connect( timeout: const Duration(seconds: 15), autoConnect: false, // true causes connection delays on Android );

    // Monitor connection state device.connectionState.listen((state) { if (state == BluetoothConnectionState.disconnected) { _scheduleReconnect(device); } });

    await _discoverServices(device); }

    Future _discoverServices(BluetoothDevice device) async { final services = await device.discoverServices();

    for (final service in services) { if (service.uuid == Guid(SERVICE_UUID)) { for (final char in service.characteristics) { if (char.uuid == Guid(TEMP_UUID)) _tempCharacteristic = char; if (char.uuid == Guid(CTRL_UUID)) _ctrlCharacteristic = char; } } } }

    // Reconnect with exponential backoff int _reconnectAttempts = 0; Future _scheduleReconnect(BluetoothDevice device) async { final delay = Duration(seconds: [2, 5, 10, 30][_reconnectAttempts.clamp(0, 3)]); await Future.delayed(delay); _reconnectAttempts++; try { await connect(device); _reconnectAttempts = 0; } catch (_) { _scheduleReconnect(device); } } }

    Step 3: Real-Time Sensor Data via Notify

    BLE notify is the equivalent of WebSocket for Bluetooth — the device pushes data to your app without polling:

    Stream streamTemperature() async* {
      if (_tempCharacteristic == null) throw StateError('Not connected');

    // Enable notifications on the characteristic await _tempCharacteristic!.setNotifyValue(true);

    await for (final value in _tempCharacteristic!.onValueReceived) { // Parse 4-byte IEEE 754 float (little-endian, as set in ESP32 firmware) final byteData = ByteData.sublistView(Uint8List.fromList(value)); yield byteData.getFloat32(0, Endian.little); } }

    On the ESP32 side, your firmware notifies every 500ms:

    // ESP32 firmware (simplified)
    float temperature = readTemperatureSensor();
    uint8_t buf[4];
    memcpy(buf, &temperature, 4);
    pTempCharacteristic->setValue(buf, 4);
    pTempCharacteristic->notify();
    

    Step 4: State Management with Riverpod

    // providers.dart
    final bleServiceProvider = Provider((ref) => BLEDeviceService());

    final temperatureProvider = StreamProvider.autoDispose((ref) { final service = ref.watch(bleServiceProvider); return service.streamTemperature(); });

    // In your widget class TemperatureWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final temp = ref.watch(temperatureProvider);

    return temp.when( data: (value) => Text( '${value.toStringAsFixed(1)}°C', style: const TextStyle(fontSize: 48, color: Colors.white), ), loading: () => const CircularProgressIndicator(), error: (e, _) => Text('Disconnected', style: TextStyle(color: Colors.red)), ); } }

    Step 5: Writing Commands to the Device

    enum DeviceCommand { on, off, reset }

    Future sendCommand(DeviceCommand cmd) async { if (_ctrlCharacteristic == null) throw StateError('Not connected');

    final byte = switch (cmd) { DeviceCommand.on => [0x01], DeviceCommand.off => [0x00], DeviceCommand.reset => [0xFF], };

    await _ctrlCharacteristic!.write(byte, withoutResponse: false); // withoutResponse: false = BLE Write With Response (guaranteed delivery) // withoutResponse: true = BLE Write Without Response (faster, no ACK) }

    Background BLE

    On iOS, background BLE requires the bluetooth-central background mode and a CoreBluetooth state restoration identifier. On Android, use a Foreground Service:

    // Start foreground service to keep BLE alive in background (Android)
    await FlutterBluePlus.setLogLevel(LogLevel.none);

    // Add to AndroidManifest.xml: //

    Production Checklist

  • Filter scan by service UUID (not all devices)
  • Handle BluetoothAdapterState.off gracefully
  • Reconnect with exponential backoff
  • Set notify value = false on disconnect
  • Test on real hardware (emulator has no BLE)
  • Test on Android 12+ (new BLUETOOTH_SCAN permission)
  • Test on iOS 16+ (CBCentralManagerOptionShowPowerAlertKey behavior)
  • Building BLE-connected IoT apps is our bread and butter at Code Caracal. [Contact us](/contact) if you need a production BLE IoT controller built.

    Written by CodeCaracal Engineering

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

    More Articles

    Business · 12 min read

    IoT Device Compliance: FCC, CE, and Product Certification Guide for Hardware Startups

    Business · 11 min read

    What to Look for When Hiring an IoT Development Partner: 8 Critical Criteria

    Business · 11 min read

    IoT MVP to Production: Realistic Timeline and Budget for Hardware Startups

    Business · 11 min read

    IoT Development Agency vs Building In-House: A Decision Framework for Founders

    IoT Dashboard · 13 min read

    Next.js IoT Analytics Dashboard: From Sensor Data to Production App

    Business · 11 min read

    How Much Does It Cost to Build an IoT Product in 2024? A Realistic Breakdown

    IoT Dashboard · 11 min read

    IoT Dashboard UX: Design Principles for Industrial Monitoring Interfaces

    IoT Dashboard · 12 min read

    Node.js WebSocket Server: The Real-Time Backend for IoT Dashboards

    Cloud & DevOps · 12 min read

    Containerizing IoT Backend Services with Docker: From Dev to Production

    IoT Dashboard · 14 min read

    Grafana + InfluxDB IoT Monitoring: Complete Production Setup Guide

    IoT Dashboard · 12 min read

    Building Real-Time IoT Dashboards with React and Recharts

    Cloud & DevOps · 13 min read

    CI/CD for Embedded Firmware: Automated Build, Test, and OTA Release Pipeline

    Mobile Development · 12 min read

    Flutter Offline-First IoT Apps: Hive + Sync Architecture That Works in the Field

    Cloud & DevOps · 14 min read

    Terraform for IoT Infrastructure: Provisioning AWS IoT Core, Lambda, and InfluxDB as Code

    Mobile Development · 10 min read

    Flutter IoT Alerts: Firebase Push Notifications for Device Events

    Cloud & DevOps · 12 min read

    Deploying IoT Backends on AWS: ECS Fargate vs Lambda vs EC2 Decision Guide

    Mobile Development · 11 min read

    Flutter + MQTT: Building Production IoT Mobile Apps That Scale

    Cloud & DevOps · 13 min read

    AWS IoT Core vs Azure IoT Hub vs Google Cloud IoT: 2024 Honest Comparison

    IoT Engineering · 13 min read

    Kafka vs RabbitMQ for IoT: Choosing the Right Message Queue for High-Volume Telemetry

    IoT Engineering · 14 min read

    IoT System Testing: Unit, Integration, Hardware-in-the-Loop, and End-to-End

    IoT Engineering · 14 min read

    Predictive Maintenance with IoT Sensor Data: From Threshold to Machine Learning

    Embedded Systems · 14 min read

    IoT Bootloader Design: Secure Boot, A/B Partitions, and Reliable OTA Recovery

    IoT Engineering · 14 min read

    Multi-Tenant IoT Platform Architecture: Isolation, Scaling, and Data Partitioning

    Embedded Systems · 14 min read

    Memory Management in Embedded Firmware: Avoiding Heap Fragmentation and Stack Overflows

    IoT Engineering · 13 min read

    IoT Cost Optimization: How We Cut AWS IoT Bills by 60% Without Sacrificing Reliability

    IoT Engineering · 12 min read

    Edge Computing in IoT: When to Process On-Device vs In the Cloud

    IoT Engineering · 13 min read

    Digital Twins for IoT: Building a Virtual Mirror of Your Physical Devices

    Embedded Systems · 14 min read

    ESP32 Deep Sleep Mastery: Cutting Power Consumption from 240mA to 10µA

    IoT Engineering · 10 min read

    MQTT QoS 0, 1, and 2 Explained: Choosing the Right Level for IoT

    IoT Engineering · 14 min read

    IoT Monitoring and Observability: Metrics, Logs, and Distributed Tracing

    Embedded Systems · 14 min read

    Debugging Embedded Firmware: JTAG, GDB, Logic Analyzers, and Serial Tracing

    IoT Engineering · 12 min read

    WebSocket vs MQTT vs Server-Sent Events: Real-Time IoT Protocol Deep Dive

    Embedded Systems · 13 min read

    STM32 HAL vs Low-Level Drivers: When the Abstraction Costs You Too Much

    IoT Engineering · 13 min read

    IoT Data Pipeline: From Raw Sensor Reading to Live Dashboard in Under 100ms

    IoT Engineering · 13 min read

    Zero-Touch IoT Device Provisioning: Scaling from 10 to 100,000 Devices

    Embedded Systems · 13 min read

    UART vs SPI vs I2C: Choosing the Right Protocol for Sensor Integration

    IoT Engineering · 12 min read

    Real-Time IoT Alerting: From Simple Thresholds to ML Anomaly Detection

    Embedded Systems · 12 min read

    ESP32 Partition Table: Designing Flash Layout for Production Firmware

    IoT Engineering · 12 min read

    IoT Architecture Patterns: Hub-and-Spoke, Mesh, and Edge-Cloud Hybrid

    Embedded Systems · 13 min read

    IoT Battery Life Optimization: Engineering Devices That Last Years on a Single Charge

    IoT Engineering · 13 min read

    Time-Series Databases for IoT: InfluxDB vs TimescaleDB vs AWS Timestream

    Security · 14 min read

    Zero-Trust Security for Embedded IoT: Why Your Devices Are Probably Vulnerable

    Embedded Systems · 14 min read

    FreeRTOS on ESP32: Task Scheduling, Queues, and Resource Management for IoT

    IoT Engineering · 12 min read

    Building a Production IoT Gateway with Raspberry Pi and Node.js

    Embedded Systems · 13 min read

    ESP32 vs STM32: Choosing the Right Microcontroller for Your IoT Project

    Mobile Development · 10 min read

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

    IoT Engineering · 13 min read

    IoT Fleet Management at Scale: AWS IoT Core Device Registry and Provisioning

    IoT Engineering · 11 min read

    MQTT vs HTTP for IoT: Which Protocol Wins in Production?

    IoT Engineering · 12 min read

    ESP32 → MQTT → AWS IoT Core: The Production-Grade Architecture Guide

    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