FlutterFlow has no built-in Bluetooth widget, so you integrate BLE via the flutter_blue_plus package in Custom Code. Export your project, add the package, write Custom Actions for scanning, connecting, and reading characteristics, then wire them back into FlutterFlow's visual UI. Test exclusively on physical hardware — web preview has no Bluetooth support.
What FlutterFlow Can and Cannot Do with Bluetooth
FlutterFlow does not include any native Bluetooth widgets or data sources. All Bluetooth functionality in a FlutterFlow app is built through Custom Code — Custom Actions and Custom Widgets that call the flutter_blue_plus Dart package. This means you need the Pro plan (code export), and you need to test on physical iOS or Android hardware. The Run Mode web preview runs in a browser where Bluetooth APIs work on very few devices and require an origin trial that most developers do not have active. This tutorial gives you working Custom Action code for all the core BLE operations — scan, connect, read, write — that you can drop into your FlutterFlow project and wire to your visual UI.
Prerequisites
- A FlutterFlow project on the Pro plan with code export enabled
- A physical Android or iOS device for testing (Bluetooth does not work in web preview)
- A BLE peripheral to test with — an Arduino, ESP32, or a commercial BLE sensor
- Basic understanding of BLE concepts: GATT, services, characteristics, UUIDs
Step-by-step guide
Add flutter_blue_plus and configure platform permissions
Add flutter_blue_plus and configure platform permissions
Add flutter_blue_plus: ^1.31.15 to your FlutterFlow project's pubspec dependencies in Settings > Pubspec Dependencies. After exporting your project, you must also edit the platform manifest files manually. For Android, add four permissions to android/app/src/main/AndroidManifest.xml: BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_SCAN, and BLUETOOTH_CONNECT (the last two are required for Android 12+). Also add ACCESS_FINE_LOCATION for scanning. For iOS, add three keys to ios/Runner/Info.plist: NSBluetoothAlwaysUsageDescription, NSBluetoothPeripheralUsageDescription, and NSLocationWhenInUseUsageDescription with user-facing reason strings. These permission strings appear in the system permission dialogs shown to users. Without them, your app will crash immediately when it attempts to use Bluetooth.
1<!-- AndroidManifest.xml additions -->2<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />3<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />4<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />5<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />6<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />Expected result: After adding the package and permissions, your app builds without errors. On first Bluetooth action, the OS shows a permission dialog to the user.
Write the scanForDevices Custom Action
Write the scanForDevices Custom Action
Create a Custom Action called scanForDevices in FlutterFlow. This action starts a BLE scan for 10 seconds, collects discovered devices, deduplicates by device ID, and returns them as a JSON array. Each device entry has the device name, device ID (hardware address on Android, UUID on iOS), RSSI (signal strength), and whether it is connectable. Wire this action to a 'Scan' button on your DeviceScanPage. Store the results in a Page State variable called discoveredDevices (type: JSON array). Bind a ListView on the page to this variable to display the scan results. Show device name, signal strength bars, and a 'Connect' button for each result. The scan runs as a Stream — start it, collect results for 10 seconds, then stop it and update the state variable.
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';23Future<List<Map<String, dynamic>>> scanForDevices() async {4 final devices = <String, Map<String, dynamic>>{};56 // Check if Bluetooth is on7 final adapterState = await FlutterBluePlus.adapterState.first;8 if (adapterState != BluetoothAdapterState.on) {9 throw Exception('Bluetooth is off. Please enable Bluetooth.');10 }1112 // Start scan13 await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));1415 // Collect scan results (deduplicated by device ID)16 await for (final results in FlutterBluePlus.scanResults) {17 for (final r in results) {18 devices[r.device.remoteId.str] = {19 'id': r.device.remoteId.str,20 'name': r.device.platformName.isNotEmpty21 ? r.device.platformName22 : 'Unknown Device',23 'rssi': r.rssi,24 'connectable': r.advertisementData.connectable,25 };26 }27 // Stop collecting after scan timeout28 if (!FlutterBluePlus.isScanningNow) break;29 }3031 await FlutterBluePlus.stopScan();32 return devices.values.toList();33}Expected result: Tapping 'Scan' on a physical device shows a list of nearby BLE peripherals within 5-10 seconds. Each entry shows the device name and a signal strength indicator.
Write the connectToDevice Custom Action
Write the connectToDevice Custom Action
Create a Custom Action called connectToDevice that accepts a deviceId String parameter. Use FlutterBluePlus.connectedDevices to check if already connected, then call device.connect() with a 10-second timeout. After connection, call device.discoverServices() and store the returned service UUIDs and characteristic UUIDs in a Page State variable for display. Create a DeviceDetailPage showing the connection status, battery level (if the standard Battery Service 0x180F is present), and a list of services with their characteristics. Store the connected device reference as a singleton in a Dart class (not in FlutterFlow state) so it persists across page navigations. Wire a 'Disconnect' button to device.disconnect() and update the connection status state variable on the connectionState stream.
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';23// Singleton to hold the connected device across page navigations4class BLEConnection {5 static BluetoothDevice? connectedDevice;6 static List<BluetoothService> services = [];7}89Future<Map<String, dynamic>?> connectToDevice(String deviceId) async {10 final device = BluetoothDevice.fromId(deviceId);1112 try {13 await device.connect(timeout: const Duration(seconds: 10));14 final services = await device.discoverServices();1516 BLEConnection.connectedDevice = device;17 BLEConnection.services = services;1819 final serviceMap = services.map((s) => {20 'serviceUuid': s.serviceUuid.str,21 'characteristics': s.characteristics.map((c) => {22 'uuid': c.characteristicUuid.str,23 'canRead': c.properties.read,24 'canWrite': c.properties.write,25 'canNotify': c.properties.notify,26 }).toList(),27 }).toList();2829 return {'deviceId': deviceId, 'services': serviceMap};30 } catch (e) {31 return {'error': e.toString()};32 }33}Expected result: Tapping 'Connect' on a device card initiates a BLE connection. After 2-5 seconds, the DeviceDetailPage shows the connected status and lists the device's services and characteristics.
Read and write GATT characteristics
Read and write GATT characteristics
Create two Custom Actions: readCharacteristic and writeCharacteristic. The read action takes a serviceUuid and characteristicUuid, finds the matching characteristic from BLEConnection.services, calls characteristic.read(), and returns the raw bytes converted to a human-readable string (UTF-8 for text, int for numeric sensors). The write action takes the same UUIDs plus a String value, encodes it to UTF-8 bytes, and calls characteristic.write(). For real-time sensor data (e.g., heart rate or temperature), use characteristic.setNotifyValue(true) and listen to characteristic.onValueReceived — this pushes data to the app without polling. Store incoming notification values in a Page State variable and display them in a large Text widget with auto-update.
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';23// Read a characteristic value once4Future<String> readCharacteristic(5 String serviceUuid,6 String characteristicUuid,7) async {8 final service = BLEConnection.services.firstWhere(9 (s) => s.serviceUuid.str.toLowerCase() == serviceUuid.toLowerCase(),10 orElse: () => throw Exception('Service not found: $serviceUuid'),11 );1213 final characteristic = service.characteristics.firstWhere(14 (c) => c.characteristicUuid.str.toLowerCase() == characteristicUuid.toLowerCase(),15 orElse: () => throw Exception('Characteristic not found: $characteristicUuid'),16 );1718 final value = await characteristic.read();19 return String.fromCharCodes(value);20}2122// Write a string value to a characteristic23Future<void> writeCharacteristic(24 String serviceUuid,25 String characteristicUuid,26 String value,27) async {28 final service = BLEConnection.services.firstWhere(29 (s) => s.serviceUuid.str.toLowerCase() == serviceUuid.toLowerCase(),30 );31 final characteristic = service.characteristics.firstWhere(32 (c) => c.characteristicUuid.str.toLowerCase() == characteristicUuid.toLowerCase(),33 );34 await characteristic.write(value.codeUnits);35}3637// Subscribe to real-time notifications from a characteristic38Future<void> subscribeToCharacteristic(39 String serviceUuid,40 String characteristicUuid,41 Function(String value) onData,42) async {43 final service = BLEConnection.services.firstWhere(44 (s) => s.serviceUuid.str.toLowerCase() == serviceUuid.toLowerCase(),45 );46 final characteristic = service.characteristics.firstWhere(47 (c) => c.characteristicUuid.str.toLowerCase() == characteristicUuid.toLowerCase(),48 );49 await characteristic.setNotifyValue(true);50 characteristic.onValueReceived.listen((value) {51 onData(String.fromCharCodes(value));52 });53}Expected result: Reading a characteristic returns its current value as a string. Writing a characteristic sends data to the peripheral. Subscribing to notifications shows real-time data updates in the UI without manual polling.
Build the sensor data display page
Build the sensor data display page
Create a SensorDataPage in FlutterFlow. Add a large Card at the top showing the connected device name and a green/red connection status indicator. Below, add a ListView of characteristic value cards — each showing the characteristic UUID, last read value, and a Refresh button. For notifying characteristics, add a 'Live' chip that subscribes to notifications when tapped. Add a LineChart widget bound to a Page State list variable that appends each incoming notification value with a timestamp — this creates a live-updating sensor chart. Add an 'Export to CSV' button that writes the stored readings to a file and shares it using the share_plus package. Disconnect and return to the scan page if the device disconnects (detected via the connectionState stream).
Expected result: The sensor data page shows live-updating values from the BLE device. The chart scrolls left as new data points arrive. The export button shares a CSV of all recorded readings.
Complete working example
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';23// Singleton for persistent BLE state across FlutterFlow pages4class BLEManager {5 static BluetoothDevice? connectedDevice;6 static List<BluetoothService> services = [];7 static bool get isConnected =>8 connectedDevice != null &&9 connectedDevice!.isConnected;1011 static void clear() {12 connectedDevice = null;13 services = [];14 }15}1617// Check Bluetooth state before any operation18Future<bool> isBluetoothOn() async {19 final state = await FlutterBluePlus.adapterState.first;20 return state == BluetoothAdapterState.on;21}2223// Scan for nearby BLE devices (10 second timeout)24Future<List<Map<String, dynamic>>> scanForDevices() async {25 if (!await isBluetoothOn()) throw Exception('Bluetooth is disabled.');2627 final found = <String, Map<String, dynamic>>{};28 await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));2930 await for (final results in FlutterBluePlus.scanResults) {31 for (final r in results) {32 found[r.device.remoteId.str] = {33 'id': r.device.remoteId.str,34 'name': r.device.platformName.isNotEmpty ? r.device.platformName : 'Unknown',35 'rssi': r.rssi,36 };37 }38 if (!FlutterBluePlus.isScanningNow) break;39 }40 return found.values.toList();41}4243// Connect and discover services44Future<bool> connectToDevice(String deviceId) async {45 final device = BluetoothDevice.fromId(deviceId);46 try {47 await device.connect(timeout: const Duration(seconds: 10));48 BLEManager.connectedDevice = device;49 BLEManager.services = await device.discoverServices();50 return true;51 } catch (e) {52 return false;53 }54}5556// Disconnect cleanly57Future<void> disconnectDevice() async {58 await BLEManager.connectedDevice?.disconnect();59 BLEManager.clear();60}6162// Read a characteristic value63Future<String> readCharacteristic(String serviceUuid, String charUuid) async {64 final s = BLEManager.services.firstWhere(65 (s) => s.serviceUuid.str.toLowerCase() == serviceUuid.toLowerCase());66 final c = s.characteristics.firstWhere(67 (c) => c.characteristicUuid.str.toLowerCase() == charUuid.toLowerCase());68 final bytes = await c.read();69 return String.fromCharCodes(bytes);70}7172// Write to a characteristic73Future<void> writeCharacteristic(String serviceUuid, String charUuid, String value) async {74 final s = BLEManager.services.firstWhere(75 (s) => s.serviceUuid.str.toLowerCase() == serviceUuid.toLowerCase());76 final c = s.characteristics.firstWhere(77 (c) => c.characteristicUuid.str.toLowerCase() == charUuid.toLowerCase());78 await c.write(value.codeUnits);79}Common mistakes
Why it's a problem: Trying to test Bluetooth features in FlutterFlow's Run Mode web preview
How to avoid: Export your project and test exclusively on a physical Android or iOS device. Use Android Studio's device manager or run directly via USB with flutter run. There is no shortcut — Bluetooth testing requires hardware.
Why it's a problem: Not requesting Bluetooth and Location permissions before starting the scan
How to avoid: Use the permission_handler package to request BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and ACCESS_FINE_LOCATION on Android, and Bluetooth usage on iOS, before making any flutter_blue_plus calls. Handle the case where the user denies permission with a clear explanation dialog.
Why it's a problem: Storing the BluetoothDevice reference in FlutterFlow App State (JSON)
How to avoid: Store the BluetoothDevice reference in a Dart singleton class (like BLEManager in the example code). This persists across FlutterFlow page navigations without serialization and gives you direct access to the live device object.
Best practices
- Always check if Bluetooth is enabled (adapterState) before starting a scan or connection
- Request all required permissions before any BLE operation using permission_handler — handle denial gracefully
- Store BluetoothDevice references in a Dart singleton, not in FlutterFlow App State JSON variables
- Stop the scan explicitly with FlutterBluePlus.stopScan() after connecting — leaving a scan running drains the battery
- Subscribe to device.connectionState stream to detect unexpected disconnections and show the user a reconnect prompt
- Test on multiple real devices from different manufacturers — BLE stack behavior varies significantly between Android brands
- Test on iOS and Android separately — Bluetooth behavior, permission timing, and UUID formats differ between platforms
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app that connects to BLE devices using flutter_blue_plus. Write a complete Dart class called BLEManager that acts as a singleton to maintain the connected BluetoothDevice reference and discovered services list across page navigations. Include methods for scanning, connecting, reading a characteristic by service UUID and characteristic UUID, and writing a string value.
In my FlutterFlow project, add a Custom Action called subscribeToCharacteristic that accepts serviceUuid and characteristicUuid Strings. It should use the flutter_blue_plus package to call setNotifyValue(true) on the characteristic and stream incoming byte values, converting each to a String and returning them via a callback for real-time display.
Frequently asked questions
Can I use Bluetooth in FlutterFlow without the Pro plan?
No. Bluetooth requires Custom Actions and Custom Widgets using the flutter_blue_plus Dart package. Adding Dart packages requires editing pubspec.yaml, which is only accessible through FlutterFlow's code export feature — available on the Pro plan.
Why does Bluetooth not work in FlutterFlow's Run Mode?
Run Mode renders your app in a web browser. Web Bluetooth has extremely limited support, requires Chrome or Edge with specific flags, and does not support the GATT profile operations that flutter_blue_plus uses. Always test Bluetooth features on a physical iOS or Android device.
What is GATT and why do I need to know about it?
GATT (Generic Attribute Profile) is the protocol that defines how BLE devices expose their data and capabilities. Your peripheral (the device you are connecting to) exposes services — logical groups of related functionality. Each service contains characteristics — the actual data values you read from or write to. To read a sensor value, you need to know the service UUID and characteristic UUID of that sensor data on your specific device.
How do I find the service and characteristic UUIDs for my BLE device?
UUIDs are documented in your peripheral's hardware documentation or datasheet. For custom Arduino/ESP32 devices, you define the UUIDs yourself in the firmware. For commercial BLE devices, check the manufacturer's SDK documentation. You can also discover them at runtime using discoverServices() — the BLEManager example code returns all service and characteristic UUIDs so you can identify them during development.
My BLE connection drops after 30 seconds on iOS. How do I fix this?
iOS aggressively terminates BLE connections for apps in the background without the 'Uses Bluetooth LE accessories' background mode enabled. Add 'bluetooth-central' to the UIBackgroundModes array in Info.plist. For foreground-only connections, iOS also has peripheral timeout policies — keep the connection active by reading or writing at least every 25 seconds if your device does not send periodic notifications.
Can I connect to multiple BLE devices at the same time?
Yes. flutter_blue_plus supports multiple concurrent connections. The BLEManager singleton pattern shown in this tutorial only handles one device, but you can extend it to a Map<String, BluetoothDevice> indexed by device ID to manage multiple simultaneous connections. iOS allows up to approximately 8 concurrent BLE connections; Android allows approximately 7.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation