Connect Bluetooth Low Energy (BLE) health monitoring devices to a FlutterFlow app using the flutter_blue_plus package in a Custom Widget. Scan for nearby devices, filter by the GATT service UUID for the device type (0x180D for heart rate, 0x1810 for blood pressure, 0x1808 for glucose), connect to the device, subscribe to characteristic notifications, and parse the incoming data bytes according to the GATT specification. Store readings in Firestore health_readings collection and display in a dashboard. Always implement auto-reconnect logic because BLE devices disconnect frequently.
Read data from BLE health devices directly in your FlutterFlow app
Medical-grade health monitoring devices — heart rate monitors, blood pressure cuffs, pulse oximeters, glucose meters, and body temperature sensors — communicate over Bluetooth Low Energy (BLE) using standardized GATT (Generic Attribute Profile) profiles. Each device type uses a specific service UUID and transmits data in a defined binary format. Your FlutterFlow app can connect to these devices, subscribe to real-time characteristic updates, and parse the binary data into meaningful readings. This tutorial covers the BLE communication pattern using flutter_blue_plus and the specific parsing logic for the two most common health devices: heart rate monitors (GATT 0x180D) and blood pressure monitors (GATT 0x1810).
Prerequisites
- A FlutterFlow project with Standard plan or higher for Custom Code
- A BLE-compatible health monitoring device for testing (or use a heart rate app on a smartwatch that exposes GATT services)
- iOS: add NSBluetoothAlwaysUsageDescription to Info.plist. Android: add BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_FINE_LOCATION permissions to AndroidManifest.xml
Step-by-step guide
Create the BLE device scanner Custom Widget
Create the BLE device scanner Custom Widget
In FlutterFlow, go to Custom Code → Custom Widgets → Add Widget. Name it BLEDeviceScanner. Add the pubspec dependency flutter_blue_plus: ^1.30.0. The widget scans for nearby BLE devices, filters by the target service UUID, and displays a list of discovered devices. Initialize FlutterBluePlus and call FlutterBluePlus.startScan(withServices: [Guid('0000180D-0000-1000-8000-00805F9B34FB')]) to scan only for heart rate devices — the long form of the service UUID 0x180D. Listen to FlutterBluePlus.scanResults and update a local state list of devices (name + RSSI signal strength). Add a Connect button next to each device. On connect tap: await device.connect(autoConnect: false). After connection, discover services with device.discoverServices(). Find the Heart Rate Measurement characteristic (UUID 0x2A37) and call characteristic.setNotifyValue(true). Listen to characteristic.onValueReceived to get real-time heart rate data bytes.
1// BLEDeviceScanner - simplified connection + data reading2import 'package:flutter_blue_plus/flutter_blue_plus.dart';34// Heart Rate service UUID and characteristic UUID5const HEART_RATE_SERVICE = '0000180d-0000-1000-8000-00805f9b34fb';6const HEART_RATE_MEASUREMENT = '00002a37-0000-1000-8000-00805f9b34fb';78Future<void> connectAndReadHeartRate(BluetoothDevice device,9 Function(int heartRate) onReading) async {10 await device.connect(autoConnect: false, timeout: Duration(seconds: 15));11 12 final services = await device.discoverServices();13 final hrService = services.firstWhere(14 (s) => s.serviceUuid.toString() == HEART_RATE_SERVICE15 );16 17 final hrChar = hrService.characteristics.firstWhere(18 (c) => c.characteristicUuid.toString() == HEART_RATE_MEASUREMENT19 );20 21 await hrChar.setNotifyValue(true);22 hrChar.onValueReceived.listen((bytes) {23 final heartRate = parseHeartRate(bytes);24 onReading(heartRate);25 });26}2728int parseHeartRate(List<int> bytes) {29 // GATT Heart Rate Measurement format:30 // Byte 0: flags. Bit 0 = 0 means HR value is UINT8, = 1 means UINT1631 final isUint16 = (bytes[0] & 0x01) != 0;32 return isUint16 ? (bytes[2] << 8) + bytes[1] : bytes[1];33}Expected result: The Custom Widget scans for nearby BLE heart rate devices, shows them in a list with signal strength, and starts streaming heart rate values after the user taps Connect.
Parse blood pressure readings from a BLE BP monitor
Parse blood pressure readings from a BLE BP monitor
Blood pressure monitors use GATT service UUID 0x1810 (Blood Pressure) and characteristic UUID 0x2A35 (Blood Pressure Measurement). Unlike heart rate which sends continuous notifications, blood pressure sends a single reading after measurement. Set characteristic.setNotifyValue(true) and wait for the onValueReceived event which fires once when the user completes a measurement on the device. Parse the bytes: Byte 0 is the flags byte. Bytes 1-2 are systolic pressure as a SFLOAT (IEEE-11073 16-bit float). Bytes 3-4 are diastolic. Bytes 5-6 are mean arterial pressure. If flag bit 2 is set, bytes 7-8 contain pulse rate. Create a Custom Function parseBPReading(List<int> bytes) that decodes the SFLOAT values: the SFLOAT format uses the upper 4 bits as a signed exponent and lower 12 bits as a mantissa. Return a Map with systolic, diastolic, and pulseRate values.
Expected result: After the user takes a measurement on the blood pressure cuff, the parsed systolic/diastolic values and pulse rate appear in the FlutterFlow app within 2 seconds.
Store readings in Firestore and display a dashboard
Store readings in Firestore and display a dashboard
After receiving and parsing a health reading in the Custom Widget callback, call a FlutterFlow Action from the Custom Widget using the widget's action callback parameter. The action: (1) creates a Firestore document in health_readings/{userId}/readings with fields: deviceType (String, 'heart_rate' or 'blood_pressure'), value (Map: {heartRate: 75} or {systolic: 120, diastolic: 80}), unit (String, 'bpm' or 'mmHg'), timestamp (Timestamp). Create a HealthDashboard page with a Backend Query on health_readings/{userId}/readings ordered by timestamp descending, limit 100. Display the latest reading prominently (large text widget). Add a Custom Widget using fl_chart LineChart or BarChart for historical trend visualization — bind to the readings list from the Backend Query. Add a 'Connect Device' button that opens a bottom sheet containing the BLEDeviceScanner Custom Widget.
Expected result: Readings from the physical BLE device appear immediately in Firestore and update the dashboard chart in real time using the Firestore real-time listener.
Handle BLE disconnections with auto-reconnect
Handle BLE disconnections with auto-reconnect
BLE devices disconnect when: the device goes out of range, the device turns off automatically after a session, the OS kills the BLE connection to save battery, or the measurement is complete (for single-reading devices). In the Custom Widget, listen to device.connectionState stream. When the state changes to BluetoothConnectionState.disconnected: update a local variable connectionStatus to 'Disconnected' and show a red indicator in the UI. Implement auto-reconnect: if autoReconnect app state is true, wait 3 seconds and call connectAndReadHeartRate again. Show a 'Reconnecting...' indicator with a CircularProgressIndicator during the attempt. After 3 failed reconnect attempts, stop retrying and show a Toast: 'Device disconnected. Tap Connect to reconnect.' Display connection status prominently in the UI — a green dot for connected, yellow for reconnecting, red for disconnected.
Expected result: When the BLE device disconnects, the app shows a clear status indicator and automatically attempts to reconnect up to 3 times before asking the user to reconnect manually.
Complete working example
1BLE HEALTH DEVICE INTEGRATION IN FLUTTERFLOW23GATT SERVICE UUIDs (common health devices):4├── 0x180D — Heart Rate Service5│ └── 0x2A37 — Heart Rate Measurement (notify)6├── 0x1810 — Blood Pressure Service7│ └── 0x2A35 — Blood Pressure Measurement (indicate)8├── 0x1808 — Glucose Service9│ └── 0x2A18 — Glucose Measurement (notify)10├── 0x1822 — Pulse Oximeter Service11│ └── 0x2A5F — PLX Spot-Check Measurement12└── 0x1809 — Health Thermometer13 └── 0x2A1C — Temperature Measurement (indicate)1415CONNECTION FLOW:161. FlutterBluePlus.startScan(withServices: [serviceUUID])172. Show scanResults list → user taps Connect183. device.connect(autoConnect: false)194. device.discoverServices()205. Find characteristic by UUID216. characteristic.setNotifyValue(true)227. Listen to characteristic.onValueReceived238. Parse bytes per GATT spec → call action callback2425DATA PARSING:26├── Heart Rate (0x2A37):27│ ├── Byte 0: flags (bit 0 = HR format: 0=uint8, 1=uint16)28│ └── Bytes 1-2: heart rate value (BPM)29│30└── Blood Pressure (0x2A35):31 ├── Byte 0: flags32 ├── Bytes 1-2: systolic (IEEE-11073 SFLOAT)33 ├── Bytes 3-4: diastolic (SFLOAT)34 └── Bytes 5-6: mean arterial pressure (SFLOAT)3536FIRESTORE STRUCTURE:37└── health_readings/{userId}/readings/{readingId}38 ├── deviceType: 'heart_rate' | 'blood_pressure'39 ├── value: { heartRate: 72 } or { systolic: 120, diastolic: 80 }40 ├── unit: 'bpm' | 'mmHg'41 ├── deviceName: 'Polar H10'42 └── timestamp: Timestamp4344CONNECTION STATUS UI:45├── Green dot: connected, streaming46├── Yellow + spinner: reconnecting47└── Red dot: disconnectedCommon mistakes
Why it's a problem: Not implementing auto-reconnect logic, leaving users with a frozen screen when the device disconnects
How to avoid: Listen to device.connectionState stream. On disconnect, update connection status UI immediately (red dot, message). Implement auto-reconnect: wait 3 seconds, attempt reconnect, retry up to 3 times. If all retries fail, show a clear manual reconnect prompt. Always show connection status prominently in the UI.
Why it's a problem: Using the short UUID form (0x180D) instead of the full GATT UUID when scanning or filtering
How to avoid: Always use the full UUID format in scan filters and when comparing service/characteristic UUIDs: '0000180d-0000-1000-8000-00805f9b34fb' (lowercase for consistent comparison).
Why it's a problem: Requesting Bluetooth permissions after the user taps Connect instead of at app startup
How to avoid: Request Bluetooth permissions at app launch in a Custom Action using the permission_handler package: await Permission.bluetooth.request() on Android, and include NSBluetoothAlwaysUsageDescription in Info.plist for iOS. Check permission status before initiating any scan.
Best practices
- Request Bluetooth permissions at app startup with an explanation of why the app needs them, before the user reaches the device connection screen
- Always display connection status prominently — green dot for connected, red for disconnected, yellow for attempting to reconnect
- Filter BLE scans by service UUID using startScan(withServices: []) to find only relevant health devices and reduce battery drain from scanning all BLE devices
- Implement auto-reconnect with maximum 3 attempts and exponential backoff, then prompt for manual reconnect to avoid draining the user's battery with endless reconnect attempts
- Store each reading in Firestore with a timestamp so users can view historical trends and export their data
- Add threshold alerts: if a heart rate reading exceeds 150 BPM or blood pressure exceeds 140/90, trigger a local notification using flutter_local_notifications
- Test BLE connectivity on physical devices only — Bluetooth simulation in iOS simulator and Android emulator is unreliable
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow health app that needs to connect to a BLE heart rate monitor using flutter_blue_plus. Explain: (1) how to scan for nearby devices filtered by the Heart Rate service UUID, (2) how to connect and subscribe to the Heart Rate Measurement characteristic, (3) how to parse the raw bytes from the characteristic (GATT 0x2A37 format), and (4) how to handle disconnections gracefully with auto-reconnect logic.
Create a Custom Widget for FlutterFlow that connects to a BLE heart rate monitor using flutter_blue_plus. The widget should: scan for devices advertising the Heart Rate service (UUID 0000180D-0000-1000-8000-00805F9B34FB), show a list of found devices, allow the user to tap a device to connect, subscribe to the Heart Rate Measurement characteristic (0x2A37), parse the bytes per GATT specification to extract the BPM value, and fire an action callback with the heart rate integer value whenever a new reading arrives.
Frequently asked questions
How do I connect a Bluetooth health device to my FlutterFlow app?
Use the flutter_blue_plus package in a Custom Widget. Add it to your pubspec dependencies (Custom Code → Pubspec Dependencies). Create a widget that: (1) scans for BLE devices filtering by the health device's GATT service UUID, (2) displays found devices in a list, (3) connects to the selected device with device.connect(), (4) discovers services and finds the measurement characteristic, (5) calls setNotifyValue(true) to subscribe to real-time updates, and (6) parses incoming data bytes according to the GATT specification.
What BLE UUID should I use for a heart rate monitor?
Heart rate monitors use the standardized Bluetooth GATT Heart Rate Profile. The service UUID is 0000180D-0000-1000-8000-00805F9B34FB (short form: 0x180D). The Heart Rate Measurement characteristic UUID is 00002A37-0000-1000-8000-00805F9B34FB (short form: 0x2A37). Filter scans by the service UUID to find only heart rate devices. The characteristic sends notifications — subscribe with setNotifyValue(true) and listen to onValueReceived for real-time BPM updates.
How do I parse BLE heart rate data bytes in Flutter?
The GATT Heart Rate Measurement characteristic sends bytes in a defined format: Byte 0 is the flags byte. Check bit 0 of flags: if 0, the heart rate value is in byte 1 as a uint8 (use bytes[1]). If bit 0 is 1, the value is a uint16 across bytes 1-2 (use (bytes[2] << 8) + bytes[1]). For most consumer heart rate monitors, the value is uint8. Example: bytes [0x04, 0x48, 0x00] has flags=0x04, HR=0x48=72 BPM. Bytes [0x01, 0x60, 0x00] has flags=0x01 (uint16), HR = (0<<8)+0x60 = 96 BPM.
Does the flutter_blue_plus package work on both iOS and Android?
Yes. flutter_blue_plus supports iOS and Android. iOS requires adding NSBluetoothAlwaysUsageDescription to Info.plist in the exported Xcode project (FlutterFlow can handle this via Settings → App Details → Permissions). Android requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_FINE_LOCATION permissions in AndroidManifest.xml. On Android 12+, also add BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions. Note: flutter_blue_plus does NOT work in the iOS Simulator or Android Emulator — test only on physical devices.
What health devices are supported using GATT profiles?
Bluetooth GATT defines standardized profiles for: Heart Rate monitors (0x180D) — chest straps, fitness trackers. Blood Pressure monitors (0x1810) — arm cuffs. Glucose meters (0x1808) — diabetic glucose monitors. Pulse oximeters (0x1822) — fingertip SpO2 monitors. Body thermometers (0x1809). Weight scales (0x181D). Lung function spirometers (0x1846). Any device that implements the standard GATT profile for its category will work with the service and characteristic UUIDs defined in the Bluetooth SIG specification.
What if I need help building a medical device integration for my FlutterFlow health app?
BLE health device integration involves parsing binary data formats, handling connection state machines, managing permissions across iOS and Android, and implementing reliable reconnect logic. RapidDev has built custom BLE integrations for health monitoring apps including heart rate monitors, blood pressure cuffs, and glucose meters. For medical-grade device integration with FDA or CE compliance considerations, professional development support is particularly important to get right.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation