Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Use Bluetooth in FlutterFlow for Device Connectivity

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 you'll learn

  • Add flutter_blue_plus to your FlutterFlow project and request the correct platform permissions
  • Write Custom Actions to scan for BLE devices and display results in a FlutterFlow ListView
  • Connect to a BLE device and discover services and characteristics
  • Read and write GATT characteristics to exchange data with a connected sensor or peripheral
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner11 min read50-65 minFlutterFlow Pro+ (code export required; physical iOS/Android device required for testing)March 2026RapidDev Engineering Team
TL;DR

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

1

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.

AndroidManifest.xml
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.

2

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.

scanForDevices.dart
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';
2
3Future<List<Map<String, dynamic>>> scanForDevices() async {
4 final devices = <String, Map<String, dynamic>>{};
5
6 // Check if Bluetooth is on
7 final adapterState = await FlutterBluePlus.adapterState.first;
8 if (adapterState != BluetoothAdapterState.on) {
9 throw Exception('Bluetooth is off. Please enable Bluetooth.');
10 }
11
12 // Start scan
13 await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));
14
15 // 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.isNotEmpty
21 ? r.device.platformName
22 : 'Unknown Device',
23 'rssi': r.rssi,
24 'connectable': r.advertisementData.connectable,
25 };
26 }
27 // Stop collecting after scan timeout
28 if (!FlutterBluePlus.isScanningNow) break;
29 }
30
31 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.

3

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.

connectToDevice.dart
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';
2
3// Singleton to hold the connected device across page navigations
4class BLEConnection {
5 static BluetoothDevice? connectedDevice;
6 static List<BluetoothService> services = [];
7}
8
9Future<Map<String, dynamic>?> connectToDevice(String deviceId) async {
10 final device = BluetoothDevice.fromId(deviceId);
11
12 try {
13 await device.connect(timeout: const Duration(seconds: 10));
14 final services = await device.discoverServices();
15
16 BLEConnection.connectedDevice = device;
17 BLEConnection.services = services;
18
19 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();
28
29 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.

4

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.

bleCharacteristics.dart
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';
2
3// Read a characteristic value once
4Future<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 );
12
13 final characteristic = service.characteristics.firstWhere(
14 (c) => c.characteristicUuid.str.toLowerCase() == characteristicUuid.toLowerCase(),
15 orElse: () => throw Exception('Characteristic not found: $characteristicUuid'),
16 );
17
18 final value = await characteristic.read();
19 return String.fromCharCodes(value);
20}
21
22// Write a string value to a characteristic
23Future<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}
36
37// Subscribe to real-time notifications from a characteristic
38Future<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.

5

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

bleManager.dart
1import 'package:flutter_blue_plus/flutter_blue_plus.dart';
2
3// Singleton for persistent BLE state across FlutterFlow pages
4class BLEManager {
5 static BluetoothDevice? connectedDevice;
6 static List<BluetoothService> services = [];
7 static bool get isConnected =>
8 connectedDevice != null &&
9 connectedDevice!.isConnected;
10
11 static void clear() {
12 connectedDevice = null;
13 services = [];
14 }
15}
16
17// Check Bluetooth state before any operation
18Future<bool> isBluetoothOn() async {
19 final state = await FlutterBluePlus.adapterState.first;
20 return state == BluetoothAdapterState.on;
21}
22
23// Scan for nearby BLE devices (10 second timeout)
24Future<List<Map<String, dynamic>>> scanForDevices() async {
25 if (!await isBluetoothOn()) throw Exception('Bluetooth is disabled.');
26
27 final found = <String, Map<String, dynamic>>{};
28 await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));
29
30 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}
42
43// Connect and discover services
44Future<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}
55
56// Disconnect cleanly
57Future<void> disconnectDevice() async {
58 await BLEManager.connectedDevice?.disconnect();
59 BLEManager.clear();
60}
61
62// Read a characteristic value
63Future<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}
71
72// Write to a characteristic
73Future<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.

ChatGPT Prompt

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.

FlutterFlow Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.