Flutter + Bluetooth

An Exciting, Hands-On Journey to Mastering Bluetooth Low Energy with Flutter

Scott Hatfield
22 min readNov 17, 2023

In our hyper-connected era, Bluetooth Low Energy (BLE) technology is absolutely everywhere. It’s the invisible thread that binds a myriad of devices, from smartwatches to refrigerators, creating a seamless bridge in the Internet of Things (IoT) universe. But, let’s face it, as universal as BLE might be, diving into the world of Bluetooth communication can often feel like navigating a labyrinth. The complexities, the nuances, the potential confusion — it’s enough to make even seasoned developers pause.

Bluetooth is the IoT development team’s collective white whale….only blue. Also hopefully it turns out better for us than it did for Ahab. Actually just forget that metaphor.

In this article, we’ll explore, experiment, and master the art of connecting, bonding, and communicating with Bluetooth Low Energy devices using Flutter. Tailored for both newcomers looking to dip their toes into BLE and seasoned developers aiming to add a dash of Flutter magic to their toolkit, this article is designed to provide everything you need to integrate Bluetooth into your Flutter application.

So grab your favorite cup of coffee ☕ or tea 🍵, settle into your coding chair, and let’s embark on this exciting adventure with Flutter and Bluetooth Low Energy!

A good dose of caffeine is essential for successfully implementing a Bluetooth communication stack.

🏁 Definition and Overview of BLE

Bluetooth Low Energy facilitates short-range wireless communication with a focus on low power consumption. It’s tailored for applications needing intermittent data transfer rather than continuous streaming. Before we jump into developing a Flutter app that uses Bluetooth communication, let’s do a quick recap on the anatomy of Bluetooth communication.

BLE Services, Characteristics, and Descriptors

In BLE communication, data structures are organized hierarchically into profiles, services, characteristics, and descriptors:

  • Profiles: A profile is a predefined collection of services that together fulfill a particular use case or functionality. It ensures that devices can understand each other and interact in a standardized way. For example, the Heart Rate Profile (HRP) defines how heart rate monitoring devices communicate with other devices, like smartphones.
  • Services: Services are collections of characteristics and serve as a way to group related functionalities together. For example, a health monitoring service might include characteristics for heart rate, temperature, and blood pressure.
  • Characteristics: Characteristics define the data types and behaviors that represent a particular feature or function of a device. Each characteristic may have one or more descriptors and can have properties like read, write, notify, etc.
  • Descriptors: Descriptors provide additional information about a characteristic, such as the measurement unit or a description of the data. They are metadata that help in understanding the characteristic’s purpose and usage.

Roles in BLE Communication

In BLE communication, devices assume distinct roles, each with its own responsibilities.

Central:

  • The central device initiates connections and controls the communication process.
  • For a Flutter app that uses BLE communication, the device on which the Flutter app runs is typically the central device.
  • It scans for nearby peripheral devices and requests connection establishment.
  • The central can read data from or write data to the peripheral’s characteristics.
  • A central can be connected to multiple peripheral devices at once.

Peripheral:

  • The peripheral device responds to connection requests from centrals.
  • It advertises its presence and services to be discovered by centrals.
  • Peripherals can’t initiate connections themselves. They only talk to central devices.
  • They expose data through services and characteristics that centrals can interact with.
  • Examples include sensors, fitness trackers, and IoT devices.
A Central device, such as a smartphone, can connect to multiple Peripheral devices.

📚 Further Reading

If you want to dive further into the details and learn more about Bluetooth Low Energy, there are a number of great articles online:

💻 Preparing Your Development Environment

In this section, we’ll cover the essential steps to set up your development environment for creating a Flutter application that utilizes Bluetooth Low Energy (BLE) communication.

Setting Up Flutter for BLE

Before you dive into developing a Flutter app with BLE capabilities, you need to install the necessary packages and configure your development environment. In this article, we will use the FlutterSplendidBLE plugin, which simplifies BLE integration in Flutter applications.

Full disclosure: I made this plugin.

This procedure assumes that you’ve already created a Flutter project.

Add the FlutterSplendidBLE Plugin

  • Open your project’s pubspec.yaml file.
  • Add flutter_splendid_ble to the list of dependencies.
dependencies:
flutter:
sdk: flutter
flutter_splendid_ble: ^latest_version
  • Save the file and run flutter pub get to fetch and add the plugin to your project.

🤝 Requesting Bluetooth Permissions

Before you begin working with Bluetooth functionality in your Flutter app, it’s crucial to ensure that your app has the necessary permissions to access Bluetooth features on various platforms. Failing to request and obtain permissions will, in the best case, make your app fail to function and, in the worse case, cause a crash. Permissions may vary depending on whether your app is running on iOS, Android, or macOS. In this section, we’ll discuss how to request Bluetooth permissions on each platform.

For example, this is an error from Android when attempting to start a Bluetooth scan before obtaining the necessary permissions.

iOS

To request Bluetooth permissions on iOS, you need to consider two primary permissions:

  • Bluetooth Permission: This permission allows your app to access the device’s Bluetooth hardware. It’s essential for scanning for nearby BLE devices, connecting to peripherals, and exchanging data.
  • Location Permission: Since BLE is closely tied to location services on iOS, you’ll often need to request location permissions as well, even if your app doesn’t directly involve location tracking. Location permission is required for scanning nearby BLE devices.

First, you will need to update your Info.plist file to include key/value pairs related to these two permissions.

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth capabilities are used to connect to BLE devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Bluetooth peripheral usage permissions are used to discover and interact with BLE devices.</string>

However, iOS handles Bluetooth permissions in a unique way. Even if you’ve included the above key/value pairs in your app’s Info.plist file to request Bluetooth and Location permissions, the operating system won’t prompt the user for these permissions until your app attempts to use Bluetooth functionality. This means that permission requests are deferred until they are needed, helping ensure a more seamless user experience.

Therefore, there are two approaches to requesting permissions on iOS, first, you can simply do nothing and, whenever your app conducts its first Bluetooth operation, the OS will request permissions from the user. Second, if you want to craft a user experience in which permissions are requested at a specific time, for example on a page that explains the need for permissions in a branded way, the FlutterSplendidBle plugin provides a method to trigger a permissions request.

/// A [Stream] used to listen for changes in the status of the Bluetooth permissions required for the app to operate
/// and set the value of [_permissionsGranted].
StreamSubscription<BluetoothPermissionStatus>? _bluetoothPermissionsStream;

/// Requests Bluetooth permissions.
///
/// The user will be prompted to allow the app to use various Bluetooth features of their mobile device. These
/// permissions must be granted for the app to function since its whole deal is doing Bluetooth stuff.
Future<void> _requestApplePermissions() async {
// Request the Bluetooth Scan permission
_bluetoothPermissionsStream = _ble.emitCurrentPermissionStatus().listen((event) {
// Check if permission has been granted or not
if (event != BluetoothPermissionStatus.granted) {
// If permission is denied, show a SnackBar with a relevant message
debugPrint('Bluetooth permissions denied or are unknown.');

// TODO probably show the user some kind of message
}
// If permissions were granted, we go on our merry way
else {
debugPrint('Bluetooth and location permissions granted.')

// TODO go ham on Bluetooth now that we have permissions
}
});
}

The emitCurrentPermissionStatus method returns a Stream<BluetoothPermissionStatus> to which a class can subscribe in order to be updated when the permissions were granted or denied. Just remember to cancel this StreamSubscription when you are done with it to avoid memory leaks.

@override
void dispose() {
_bluetoothPermissionsStream?.cancel();

super.dispose();
}

Android

On Android, requesting permissions is a little more straightforward than on iOS because an app can directly trigger the permissions request on demand.

To request Bluetooth permissions on Android, you can use the permission_handler package:

import 'package:permission_handler/permission_handler.dart';
// Request Bluetooth permission
PermissionStatus status = await Permission.bluetooth.request();

Note that the FlutterSplendidBle package already handles adding the necessary permissions to the AndroidManifest.xml file.

Once permissions have been granted, we can get on with the fun stuff.

⚡Checking the Bluetooth Adapter

This will be a short section that focuses more on creating a good user experience in an app that utilizes Bluetooth communication rather than focusing on technical implementation details. Before attempting to use the Bluetooth adapter on the host device, it it probably a good idea to check and verify that the adapter is enabled and available for use by the app. This way, if your app finds that Bluetooth is unavailable, the user can be prompted to, for example, turn on Bluetooth.

The FlutterSplendidBle plugin provides a method to check the status of the Bluetooth adapter and listen for changes in the status.

/// Checks the status of the Bluetooth adapter on the host device (assuming one is present).
///
/// Before the Bluetooth scan can be started or any other Bluetooth operations can be performed, the Bluetooth
/// capabilities of the host device must be available. This method establishes a listener on the current state
/// of the host device's Bluetooth adapter, which is represented by the enum, [BluetoothState].
void _checkAdapterStatus() async {
try {
_bluetoothStatusStream = _ble.emitCurrentBluetoothStatus().listen((status) {
// TODO since Bluetooth is enabled, you can do other BLE operations. Perhaps set a flag here?
});
} catch (e) {
debugPrint('Unable to get Bluetooth status with exception, $e');

// TODO handle this error in some way
}
}

There are three possible values that communicate the status of the Bluetooth adapter: enabled, disabled, or notAvailable.

The emitCurrentBluetoothStatus method returns a Stream<BluetoothStatus> so ensure that any listeners to this stream are canceled when they are no longer needed, for example when the class is disposed. This is a pattern that we will use throughout this article by the way.

@override
void dispose() {
_bluetoothStatusStream?.cancel();

super.dispose();
}

📡 Discovering BLE Peripherals

Scanning is the initial step in interacting with BLE peripherals. During the scanning process, your Flutter app will search for nearby BLE devices that are advertising their presence. Each BLE device broadcasts advertisements at regular intervals, containing essential information that allows your app to identify and connect to the desired peripheral.

Let’s discuss how BLE peripherals announce their existence to the world.

BLE Advertisement Data: What’s in It?

BLE advertisement data is a critical component of the BLE communication process. It serves as a beacon that BLE peripherals broadcast periodically. This data plays a vital role in enabling your app to discover and identify nearby BLE devices. Here’s a breakdown of what you can find in BLE advertisement data:

  • Device Identifier: Each BLE device advertises a unique identifier known as the “Device Address” or “MAC Address.” This identifier distinguishes one device from another, allowing your app to recognize and connect to specific peripherals. The first part of this identifier often contains a unique code assigned to the manufacturer of the Bluetooth device or the Bluetooth radio within the device. This manufacturer-specific part of the identifier provides additional context about the device’s origin.
  • Device Name: Many BLE devices include a human-readable name in their advertisements. This name can be particularly helpful for users to identify the device they want to connect to. For example, “Heart Rate Monitor.” Typically, this is the name that you would see in a scan for nearby Bluetooth devices.
  • Service UUIDs: BLE peripherals often advertise the Universally Unique Identifier (UUID) of the services they offer. Services define the functionalities a device provides. By examining these service UUIDs, your app can determine if a discovered device offers the desired features. For instance, a heart rate monitor service might have a specific UUID (e.g., “0000180D-0000–1000–8000–00805F9B34FB”).
  • Signal Strength (RSSI): The Received Signal Strength Indicator (RSSI) is a measure of how strong the signal is from the discovered device. It quantifies the proximity of the device to your app. A stronger RSSI generally indicates a closer device, while a weaker signal suggests a more distant one. However, since transmission strength among Bluetooth devices can vary, distance cannot always be infered from RSSI without some additional infrastructure.

Implementing BLE Scanning in Flutter with FlutterSplendidBLE

Now that we’ve covered the basics of Bluetooth advertisement and discovery, let’s go through the steps to implement BLE scanning in a Flutter app using the FlutterSplendidBLE plugin:

1. Initialize the plugin: You will need an instance of the FlutterSplendidBle class, through which much of the functionality from the plugin will be accessed. So, in the class where you want to conduct the Bluetooth scan, create an instance of the FlutterSplendidBle class. For the architecturally-minded among you, it may be desirable to create this class in a central place, like a BLE service of some kind.

/// A [FlutterBle] instance used for Bluetooth operations conducted by this route.
final FlutterSplendidBle _ble = FlutterSplendidBle();

2. Start scanning: Use the plugin’s API to start scanning for BLE devices. Depending upon how your app is designed, you may do this through the initState function so that scanning begins as soon as a screen loads, or you may do this in response to some user interaction like a button tap.

/// A [StreamSubscription] for the Bluetooth scanning process.
StreamSubscription<BleDevice>? _scanStream;

/// Starts a Bluetooth scan with a time limit of four seconds, after which the scan is stopped to
/// save on battery life.
void startScan() {
// Start the Bluetooth scan and listen for results or errors.
_scanStream = _ble.startScan().listen(
(device) => _onDeviceDetected(device),
onError: (error) {
// Handle the error here
_handleScanError(error);
return;
},
);

// After four seconds, stop the scan.
Future.delayed(const Duration(seconds: 4), () {
if(mounted) {
stopScan();
}
});
}

Alright, let’s go though this function line by line. First, we create a StreamSubscription that will provide events (newly discovered devices) to listeners. The major reason for declaring this instance within the class in which scanning takes place is so that the stream can be cancelled later. We must remember to do this to avoid memory leaks, but we will cover this in more detail in a second.

Next we start the scan using the startScan method from the FlutterSplendidBle plugin. We will also set a listener for results from the scan using the listen method. This method returns a StreamSubscription<BleDevice> that we will assign to the _scanStream private variable we just defined.

Two different things might come out of this stream. First, of course, each newly discovered BLE device will be emitted to the stream. This function will call the _onDeviceDetected method for each BleDevice instance emitted from the stream. Second, we might get some kind of error from the attempt to start a Bluetooth scan. In this case, the onError callback is invoked and the function, in turn, calls the _handleScanError method. We will define these methods in a moment.

Finally, we create a Future that will be used to stop the scan after four seconds. We don’t want the scan to just continue forever because it can use a lot of battery power and make our users mad.

When building an app that uses Bluetooth, it is always important to not ruin your users’ battery lives. Otherwise they will get really mad, leave negative reviews for your app, and you will be fired and possibly imprisoned, probably.

The four second timeout is not set in stone or even defined in Bluetooth specs anywhere. A four-second timeout strikes a balance between discovering nearby Bluetooth devices within a reasonable time and conserving power and is a commonly used timeout value. Longer scanning durations might consume more battery, while shorter durations might miss some devices. But, depending upon your application, you may want a shorter or longer scan duration.

3. Handle Scan Results: The scanning process will emit BleDevice instances on the Steam. In the code snippet above showing the startScan method, we set a callback function for handling newly discovered devices. That callback function could look something like this:

/// A list of [BleDevice]s discovered by the Bluetooth scan.
List<BleDevice> discoveredDevices = [];

/// Handles newly discovered devices by adding them to the [discoveredDevices] list and triggering a rebuild
/// of the [HomeView].
void _onDeviceDetected(BleDevice device) {
setState(() {
discoveredDevices.add(device);
});
}

This _onDeviceDetected callback simply updates a list of discovered devices and triggers a rebuild of the widget. However, there are other actions you may want to take in this callback, such as logging the devices for analytics purposes, updating some kind of timeout used to determine if no devices were detected, or update the UI in ways other than just updating the list.

We discovered some stuff. I guess there are a lot of headphones in coffee shop.

4. Stop Scanning: At some point, all good Bluetooth scans must come to an end. In the code snippet above, there is a timeout of four seconds used to automatically end the scan. However, you may also want to end the scan for other reasons, for example in response to the user tapping a button, when a specific device or type of device is detected by the scan, when a user selects a device from the list, or, very importantly, when the class handling the scan is disposed.

Stopping the scan itself involves simply calling the stopScan method from the FlutterSplendidBle plugin but it is also important that the StreamSubscription is canceled to avoid memory leaks.

/// Assuming there is a scan ongoing, this function can be used to stop the scan. If this is called while a scan
/// is not happening, nothing will happen.
void stopScan() {
_ble.stopScan();
_scanStream?.cancel();
}

For the latter point about canceling the scan when the class is disposed, this can be handled by simply calling the stopScan method in the onDispose method for the widget.

  @override
void dispose() {
stopScan();

super.dispose();
}

5. Handling Errors: If there is an error related to starting the Bluetooth scan, such as a problem with permissions, with the host device’s hardware, or other issues, errors will be emitted on the stream. In the code snippet above, these will invoke the onError callback. There are, of course, many, many different ways to handle errors that will depend heavily on your application and your target user base. Since these code snippets are coming from the FlutterSplendidBle plugin’s scanning example, which is intended for developers, the error is shown in the SnackBar. Of course, in the real world, you will probably want a different implementation and may also want to include some kind of logging.

/// Handles errors emitted by the [_scanStream] from attempting to start a Bluetooth scan.
void _handleScanError(error) {
// Create the SnackBar with the error message
final snackBar = SnackBar(
content: Text('Error scanning for Bluetooth devices: $error'),
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
// If you need to do anything when the user dismisses the SnackBar
},
),
);

// Show the SnackBar using the ScaffoldMessenger
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

🔗 Establishing a BLE Connection

Once a Bluetooth peripheral has been selected via the scanning process we just covered, or targeted in other ways, we can begin the essential process of establishing a connection between your Flutter app, acting as a Bluetooth Low Energy (BLE) central device, and a discovered BLE peripheral. The first thing you need to know is that several separate process are often lumped into the under “connecting” umbrella: the connection itself, service discovery, pairing, and bonding. However, in the interest of clearly explaining each part, they will be covered in separate sections in this article.

Therefore, the pure process of connecting to a BLE peripheral is actually fairly easy. The FlutterSplendidBle plugin provides a method for triggering the connection process and listening for results.

/// Starts the process of connecting to the BLE device. The connection status is emitted
/// on a stream.
void connect() {
try {
_connectionStateStream = _ble
.connect(deviceAddress: widget.device.address)
.listen((state) => onConnectionStateUpdate(state), onError: (error) {
// Handle the error here
_handleConnectionError(error);
});
} catch (e) {
debugPrint('Failed to connect to device, ${widget.device.address}, with exception, $e');
}
}

The connect method accepts the address of the Bluetooth device to which the app should connect. The method returns a Stream<BleConnectionState>, allowing the caller to listen for changes in the connection state. There are five possible connection states between the Flutter app and the BLE peripheral:

  • connected: The Bluetooth peripheral is successfully connected.
  • disconnected: The Bluetooth peripheral is disconnected from the host device.
  • connecting: A connection between the peripheral and the host device is in the process of being established.
  • disconnecting: The peripheral and the host device are in the process of disconnecting.
  • unknown: The connection, or lack thereof, between the peripheral and the host device is in an unknown state.

As with the other stream-based methods covered in this article, make sure to dispose of the StreamSubscription for the connection state when it is no longer needed. But, for the connection state, the decision of when to cancel the stream is a little more complicated than it is for some other steps because it may be desirable to keep listening for connection state updates throughout the app’s lifecycle. However, when it comes time to cancel the subscription, the process is the same as all the other streams.

_connectionStateStream?.cancel();

🔍 Performing Service Discovery

After a Bluetooth peripheral is successfully connected to the Flutter app, the typical next step is to perform service discovery. BLE service discovery is a crucial step in BLE communication, where a central device (the Flutter app in our case) discovers the services and characteristics offered by a peripheral device. This process is essential for two main reasons:

  1. Identification of Functionalities: Services and characteristics define what the peripheral can do. For example, a heart rate monitor offers a “Heart Rate Service” with characteristics like heart rate measurement and body sensor location.
  2. Enabling Communication: Once the services and characteristics are discovered, the central device can read, write, or subscribe to these characteristics to exchange data with the peripheral.

And wouldn’t you know it, the FlutterSplendidBle plugin provides methods for starting the service discovery process and listening for results.

/// A [StreamController] used to listen for updates during the BLE service discovery process.
StreamSubscription<List<BleService>>? _servicesDiscoveredStream;

/// Starts the service discovery process and allows the caller to subscribe to a Stream
/// in order to listen to the results of the process.
void discoverServices() {
_servicesDiscoveredStream = _ble.discoverServices(widget.device.address).listen(
(service) => _onServiceDiscovered(service),
);
}

The service discovery process results in obtaining a list of BleService instances that represent individual services from the BLE peripheral. Each BleService should contain one or more characteristics.

Remember to cancel that StreamSubscription once the service discovery process is complete.

_servicesDiscoveredStream?.cancel();

❤️ Pairing and Bonding

If there’s one thing people are probably familiar with when it comes to Bluetooth communication, it is probably the pairing and bonding process. This is where you get a popup on your phone or other device asking if you want to pair to a Bluetooth peripheral.

Bluetooth Low Energy (BLE) provides security features through pairing and bonding processes. And while these two steps are often conflated, and, in fact, are both accomplished at the same time with the “pair” button in OS popups like the one above, they are actually separate processes.

Introduction to BLE Pairing and Bonding

  • Pairing: Pairing is the process where two BLE devices establish a trusted relationship by exchanging security keys. This process occurs when a connection is first made, typically involving an exchange of keys and possibly a PIN code.
  • Bonding: Bonding is the step beyond pairing. When devices bond, they store the keys exchanged during the pairing process for future use. This means that once devices are bonded, they can establish secure connections in the future without needing to pair again. In other words, bonding prevents that popup above from appearing every single time you connect to a Bluetooth device.

BLE Characteristic Properties and Security

BLE characteristics have permissions defining the operations that can be performed on them, like read, write, notify, etc. Some of these characteristics may require encryption, necessitating pairing or bonding.

  • Encrypted Characteristics: If a characteristic requires encrypted communication, any attempt to read or write to it will necessitate a secure connection.
  • Characteristic Properties: These properties (like read, write, notify, etc.) indicate what operations are supported. The need for encryption is usually indicated by properties like readEncrypted, writeEncrypted, or similar.

Pairing/Bonding Prompt Triggered by OS

When an app tries to interact with an encrypted characteristic (e.g., reading or writing), the host device’s OS automatically prompts the user to pair with the BLE peripheral. This process is generally handled by the OS and doesn’t require explicit handling in the app.

Implementing Pairing/Bonding in FlutterSplendidBle

When using the FlutterSplendidBle plugin, triggering pairing/bonding is implicitly handled when interacting with characteristics that require secure connections.

  1. Check for Encryption Requirements: Optionally, and depending upon the UX requirements for your app, before attempting to read or write to a characteristic, you can check its properties so that your app can know whether or not pairing is required for communication.
/// Determines if a [BleCharacteristic] requires pairing/bonding for read or write operations.
bool requiresPairing(BleCharacteristic characteristic) {
bool? requiresPairingForRead = characteristic.permissions?.contains(BleCharacteristicPermission.readEncrypted);
bool? requiresPairingForWrite = characteristic.permissions?.contains(BleCharacteristicPermission.writeEncrypted);
bool? requiresPairingForReadMitm =
characteristic.permissions?.contains(BleCharacteristicPermission.readEncryptedMitm);
bool? requiresPairingForWriteMitm =
characteristic.permissions?.contains(BleCharacteristicPermission.writeEncryptedMitm);

return requiresPairingForRead == true ||
requiresPairingForWrite == true ||
requiresPairingForReadMitm == true ||
requiresPairingForWriteMitm == true;
}

2. Attempt to Interact with the Characteristic: Simply reading or writing to an encrypted characteristic can trigger the pairing process.

// This will prompt the OS to initiate pairing if not already paired
await characteristic.readValue<BleCharacteristicValue>();

3. Handling Pairing Interface: The pairing interface is usually handled by the OS. Your app might need to handle any callbacks or events that are triggered post-pairing.

And with that, your BLE peripheral is now connected, paired, and bonded to your Flutter app. Plus your Flutter app has performed service discovery so it understands the capabilities of the peripheral.

Your Flutter app and your BLE peripheral are now BFFs.

💬 Reading from and Writing to BLE Characteristics

Bluetooth Low Energy characteristics come with various properties defining the ways in which a central and a peripheral can interact. Each property serves a specific function and is suitable for different types of Bluetooth devices. Here’s a detailed look at each property as represented in FlutterSplendidBle:

broadcast

  • Description: Allows the characteristic to broadcast its value to multiple devices.
  • Example Use: A temperature sensor broadcasting readings to multiple devices simultaneously.

read

  • Description: Enables the characteristic’s value to be read by a connected device.
  • Example Use: A fitness tracker where a connected smartphone reads step count data.

noResponse

  • Description: Permits writing to the characteristic without requiring the peripheral to send a response.
  • Example Use: Sending frequent updates to a smart light bulb, where speed is more crucial than confirmation of each message.

write

  • Description: Allows writing data to the characteristic with a response from the peripheral to confirm receipt.
  • Example Use: Writing a new threshold value to a smart thermostat, where confirmation of the change is important.

notify

  • Description: Enables the characteristic to notify a connected device when its value changes.
  • Example Use: A heart rate monitor that notifies a connected device whenever the heart rate changes.

indicate

  • Description: Similar to notify, but it requires acknowledgment from the connected device, ensuring the data was received.
  • Example Use: A medical device that sends critical health data, where confirmation of receipt is essential.

signedWrite

  • Description: Supports writing to the characteristic with an authenticated signature for increased security.
  • Example Use: A secure door lock where commands to unlock should be securely signed to prevent unauthorized access.

props

  • Description: Refers to extended properties providing more details about the characteristic’s behaviors.
  • Example Use: Advanced BLE devices where characteristics have complex behaviors requiring additional descriptors.

reliableWrite

  • Description: Enables reliable writing, ensuring complete and correct transfer of a long piece of data.
  • Example Use: Transferring firmware updates to a BLE device, where the integrity of the entire message is crucial.

writableAuxiliaries

  • Description: Indicates the presence of auxiliary descriptors that can be written to.
  • Example Use: A device with configurable settings that are stored in auxiliary descriptors.

Each of these properties tailors the BLE characteristic for specific use cases, ensuring flexibility and efficiency in various scenarios. However, at the end of the day, they all amount to three different operations: reading, writing, and subscribing. The first two of these will be covered in this section with subscriptions, a slightly more advanced topic, reserved for the next section.

Reading from BLE Characteristics

Reading from a BLE characteristic delivers the current value of that characteristic to the Flutter app. The process consists of two main steps. First, the value of the characteristic is obtained in one of three raw formats supported by the FlutterSplendidBle plugin. Second, if necessary, the Flutter app can convert the value into a different format.

/// Reads the value of the provided Bluetooth characteristic and returns the value as a String.
///
/// If reading the characteristic value is successful, this function returns the characteristic value as a
/// [BleCharacteristicValue].
Future<void> _readCharacteristicValue(BleCharacteristic characteristic) async {
try {
BleCharacteristicValue characteristicValue = await characteristic.readValue<BleCharacteristicValue>();

setState(() {
_characteristicValue = characteristicValue;
});
} catch (e) {
debugPrint('Failed to read characteristic value with exception, $e');

// TODO show SnackBar?
}
}

In this code snippet, the readValue method from the FlutterSplendidBle plugin is used to obtain the current value of the characteristic. The type annotation on the method determines the type used for the returned value. Three types are supported by the plugin:

  • String: A plain, old, run-of-the-mill string.
  • BleCharacteristicValue: A class provided by the FlutterSplendidBle plugin that contains not only the actual characteristic value but also metadata about the characteristic.
  • List<int>: This is the raw data obtained from the BLE peripheral as it is typically communicated

Depending upon the API used by the BLE peripheral with which you are interacting, the data might be communicated in a different format, for example JSON or Protobufs. In this case, the type returned by the readValue method can be converted as appropriate for your application.

✉️ Subscribing to a Characteristic

And now we come to the final section we will cover in this article, about listening for updates from a BLE characteristic. BLE characteristics with notification or indication properties allow a peripheral to send updates to a connected central device asynchronously. This feature is essential for real-time data monitoring without needing to poll the peripheral constantly. Notifications and indications are similar but have a key difference:

  • Notifications: The peripheral sends updates without requiring an acknowledgment from the central device.
  • Indications: Similar to notifications, but they require an acknowledgment from the central device, ensuring the message was received.

Implementing Subscriptions in Flutter with FlutterSplendidBle

Subscribing to a characteristic involves setting up a Stream and corresponding StreamSubscription that enable the Flutter app to invoke a callback whenever the value of the characteristic changes.

/// A [StreamSubscription] used to listen for changes in the value of the characteristic.
StreamSubscription<BleCharacteristicValue>? _characteristicValueListener;

/// Subscribes to a BLE characteristic so that the app can receive updates whenever
/// the value of the characteristic changes.
void subscribeToCharacteristic() {
// Set a lister for changes in the characteristic value
_characteristicValueListener = widget.characteristic.subscribe().listen(
(event) => onCharacteristicChanged(event),
);
}

The subscribe method of the BleCharacteristic class enables a caller to set a callback for when the value of the characteristic changes. This way, your app does not need to constatly read from the characteristic to stay updated on its latest value.

As always, remember to cancel the StreamSubscription when it is no longer needed. Additionally, you should unsubscribe from the characteristic so that resources are freed up on the peripheral side of things. This can be done using the unsubscribe method of the BleCharacteristic class.

@override
void dispose() {
// Make sure that the listener is cleaned up.
widget.characteristic.unsubscribe();
_characteristicValueListener?.cancel();

super.dispose();
}

🏁 Conclusion: Building a Comprehensive Flutter BLE Application

Throughout this article, we have journeyed through the key steps involved in building a robust Flutter application capable of communicating with Bluetooth Low Energy (BLE) devices. From setting up the initial environment to handling complex operations like service discovery, reading and writing to characteristics, and managing notifications and indications, we’ve covered the foundational aspects that any developer needs to know to create effective BLE-enabled applications using Flutter.

Congratulations. You are now an expert on Bluetooth Low Energy.

Key Takeaways:

  1. Initial Setup and Permissions: We started by setting up our Flutter environment and ensuring our app has the necessary permissions to interact with BLE devices.
  2. Connecting to Devices: We then moved on to scanning for and connecting to BLE peripherals, a crucial step in establishing communication.
  3. Discovering Services and Characteristics: Understanding and implementing service discovery allows our app to interact with the specific functionalities offered by a BLE device.
  4. Reading and Writing Data: We explored how to read from and write to BLE characteristics, considering different data formats and the nuances of BLE data transmission.
  5. Handling Notifications and Indications: Finally, we delved into subscribing to characteristic updates for real-time data communication, essential for applications that require continuous monitoring or interaction with the BLE device.
This is mainly here because I need a cover photo for this article. Dall-E 3 really is getting good though.

Thanks for reading and happy Fluttering!

--

--

Scott Hatfield

I like to take big, complicated projects, and break them down into simple steps that anybody can understand. https://toglefritz.com/