BLE GATT with 🛩 Compose & Kotlin Channel 🛣️ / Flow 🌊

Siamak (Ash) Ashrafi
5 min readMay 10, 2022

--

TI BLE SensorTag / Google Edge TPU / TensorFlow Edge

Bluetooth Low Energy (BLE) on Android

BLE with Jetpack Arch Comp, Compose & Kotlin Channel / Flow

Modern Android development makes BLE not a complete nightmare 🎉.

  • BLE is a very important part of mobile development.

We do not need to learn all this … Juul Labs has done all the hard work.

  • JUUL Labs Kable BLE Android library!
  • Example: Jetpack Compose using Kable to communicate with SensorTag

Code Placement: Optimizing for testability and multi-platform.

  • Put all the code in the Repository.
  • If the UI needs data place it the ModelView as MutableStateFlow.
  • Keep the UI (Composables & MainActivity) as flat/thin as possible.

1) Notes on permissions

Permissions are usually a pain but we are building with Compose so … using Accompanist permissions API it becomes super easy 🍾.

permission = Manifest.permission.BLUETOOTH_ADMIN

2) Notes on finding Bluetooth devices.

We have three layers that need to communicate to show the found Bluetooth devices to the user.

  1. UI @ Compose layer — Shows the list of Bluetooth devices
  2. Devices @ ModelView layer — Get the devices from the Repository
  3. Data @ Repository layer — Communicate with Bluetooth radio

We make a call to get all the bluetooth devices (Repository layer 3) which adds the device to a MutableStateFlow<List<BluetoothDevice>> (ModelView Layer 2) and updates the UI (Composable Layer 1) with all the Bluetooth devices found.

Super easy! But if that is too much work you can just use the built in CompanionDeviceManager 👍🏾

On the Android app side, we now (starting in Android 8.0) have two options for discovering a device. The first is to use the BluetoothLeScanner, and the second is to use the CompanionDeviceManager. For a BLE peripheral that you will pair up with your app, it is strongly recommended to use the CompanionDeviceManager today. The BluetoothLeScanner is more useful for scanning for BLE Beacons.

3) Notes on connecting to the GATT server

From the list of Bluetooth devices choose the BLE `device` and call connectGatt passing in the `bluetoothGattCallback`.

BluetoothGatt gatt = device.connectGatt(context, false, bluetoothGattCallback, TRANSPORT_LE);

The bluetoothGattCallback will contain the Kotlin channel which will allow us to communicate in a serial way with the GATT server.

private val channel = Channel<BluetoothResult>()`

4) Notes on reading and writing characteristics

  1. Read/write operations are asynchronous.

onCharacteristicRead() and onCharacteristicWrite() return immediately but the data will not be ready so we must wait on it. The best way to do this is use a Kotlin Channel. https://getaround.tech/bluetooth-and-coroutines/

2. You can only do 1 asynchronous operation at a time.

Even if you are talking to two different services, you can only do one onCharacteristicRead/Write at a time. If you call one without the other finishing the GATT server will go into an indeterminate state.

Here is our issue: when we read/write a characteristic to the Bluetooth device to send a command, we want to wait for the device’s acknowledgement to continue. In other words, we want to communicate synchronously with the device.
To do so, we need to block the execution until onCharacteristicRead/Write is called back for my characteristic.

https://getaround.tech/bluetooth-and-coroutines/override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
// handleCharacteristic
channel.trySend(
BluetoothResult(
characteristic.uuid,
characteristic.value,
status
)
).isSuccess

}

and reading from the channel as shown in the getaround post.

private suspend fun BluetoothGatt.readCharacteristic(
serviceUUID: UUID,
characteristicUUID: UUID
): BLEFlowCallback.BluetoothResult {
...
readCharacteristic(characteristic)
return waitForResult(characteristicUUID)
}
private suspend fun waitForResult(uuid: UUID): BluetoothResult {
return withTimeoutOrNull(TimeUnit.SECONDS.toMillis(3)) {
var bluetoothResult: BluetoothResult = channel.receive()
bluetoothResult
} ?: run {
throw BluetoothTimeoutException()
}
}

Example:

That’s it … super easy!

If you would like to see an example of this working watch the video 🎞 here:

  • App on Google Play Store

--

--