BluetoothGatt is an Android class that provides an API for GATT (Generic Attribute Profile) client operations over a BLE connection. BluetoothGatt encapsulates the connection to a remote GATT server (peripheral BLE device) and manages all profile operations: service discovery, reading and writing characteristics, subscribing to notifications and indications. A BluetoothGatt instance is obtained via BluetoothDevice.connectGatt() with a BluetoothGattCallback. According to Android Developers, 2026, BluetoothGatt is the central class for bidirectional BLE communication, supporting GATT operations from BLE 4.0 to BLE 5.4.
Key Takeaways
BluetoothGatt is a proxy object representing a GATT connection between an Android device (central) and BLE peripheral (server). Each BluetoothGatt instance corresponds to one active BLE connection. All GATT profile operations are performed through it: discovery, reading, writing, notifications. BluetoothGatt is not created directly — it is returned by the BluetoothDevice.connectGatt() method.
Creating a BluetoothGatt requires four parameters. Context — the application context (Activity or Application). autoConnect — if false, Android immediately initiates a direct connection; if true, Android connects automatically when the device is detected (useful for background connection). BluetoothGattCallback — mandatory callback for all GATT events. transport — BluetoothDevice.TRANSPORT_LE (BLE) or TRANSPORT_BREDR (Classic). Always use TRANSPORT_LE on BLE devices.
BluetoothGatt lifecycle consists of five states. DISCONNECTED — initial state. CONNECTING — after calling connectGatt, before confirmation. CONNECTED — after onConnectionStateChange with STATE_CONNECTED. After connection, discoverServices() is called to obtain the GATT hierarchy. After finishing work — disconnect() and close() to free system resources. Without calling close(), the application may exhaust the Android BLE connection limit (usually 4–8).
// Creating BluetoothGatt connection
import android.bluetooth.*
class GattConnector(private val context: Context) {
private var bluetoothGatt: BluetoothGatt? = null
fun connect(device: BluetoothDevice): BluetoothGatt? {
// Close previous connection if exists
close()
bluetoothGatt = device.connectGatt(
context,
false, // autoConnect = false ( )
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
// 2. Connection established → Discovery
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
// 3. Connection lost
close()
}
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt, status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 4. GATT hierarchy received
onGattReady(gatt)
}
}
},
BluetoothDevice.TRANSPORT_LE
)
return bluetoothGatt
}
private fun onGattReady(gatt: BluetoothGatt) {
// GATT ready for read/write operations
}
fun close() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
}
}
The GattConnector class demonstrates proper BluetoothGatt creation. The autoConnect=false parameter means a direct connection (for scanned devices). On onConnectionStateChange with STATE_CONNECTED, discoverServices() is called immediately. onServicesDiscovered signals that GATT is ready. close() sequentially calls disconnect() and close() — without close() system resources are not freed, leading to BLE connection leaks.
discoverServices() is the first GATT method called after connection. It initiates an asynchronous search of all services on the BLE peripheral. The result arrives in onServicesDiscovered() with a status code: GATT_SUCCESS (0) — success, 133 — GATT_ERROR, 8 — GATT_CONNECTION_TIMEOUT. After successful discovery, BluetoothGatt populates the list of services accessible via getServices().
Each BluetoothGattService contains a list of BluetoothGattCharacteristic. A characteristic has a UUID, properties (PROPERTY_READ, PROPERTY_WRITE, PROPERTY_NOTIFY) and optional descriptors. Properties determine which operations are allowed: if a characteristic does not have PROPERTY_READ, calling readCharacteristic will return an error. Use getDescriptors() to obtain characteristic descriptors.
// Service discovery and characteristic search
class GattServiceExplorer {
// Find service by UUID
fun findService(gatt: BluetoothGatt, uuid: UUID): BluetoothGattService? {
return gatt.services?.firstOrNull { it.uuid == uuid }
}
// Find characteristic in service
fun findCharacteristic(
service: BluetoothGattService,
uuid: UUID
): BluetoothGattCharacteristic? {
return service.characteristics?.firstOrNull { it.uuid == uuid }
}
// Get all supported characteristic operations
fun getCharacteristicProperties(chars: BluetoothGattCharacteristic): List<String> {
val props = mutableListOf<String>()
with(chars.properties) {
if (and(BluetoothGattCharacteristic.PROPERTY_READ) != 0) props.add("READ")
if (and(BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) props.add("WRITE")
if (and(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) props.add("WRITE_NO_RESP")
if (and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) props.add("NOTIFY")
if (and(BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) props.add("INDICATE")
}
return props
}
// Log entire GATT hierarchy
fun dumpGattTree(gatt: BluetoothGatt) {
gatt.services?.forEach { service ->
print("Service: ${service.uuid}")
service.characteristics?.forEach { char ->
print(" Characteristic: ${char.uuid}, properties: ${char.properties}")
char.descriptors?.forEach { desc ->
print(" Descriptor: ${desc.uuid}")
}
}
}
}
}
The GattServiceExplorer class provides utilities for navigating the GATT hierarchy. findService and findCharacteristic search for services and characteristics by UUID. getCharacteristicProperties checks bit masks of properties via and(). dumpGattTree prints the full hierarchy to the log — useful when debugging BLE devices. All operations on BluetoothGatt must be performed after successful onServicesDiscovered, otherwise getServices() will return an empty list.
readCharacteristic() is an asynchronous BluetoothGatt method for reading a characteristic value from a remote BLE device. The result arrives in onCharacteristicRead() of BluetoothGattCallback. If a device has 2+ matching characteristics (unlikely but possible), readCharacteristic() may read a non-target one — it is safer to call readCharacteristic() on a BluetoothGattCharacteristic instance rather than by UUID.
readDescriptor() is a method for reading a characteristic descriptor value. A typical descriptor is CCCD (Client Characteristic Configuration Descriptor, UUID 0x2902), which determines whether notifications are enabled. The result is in onDescriptorRead(). Reading descriptors is rarely needed in practice — CCCD is managed by setCharacteristicNotification(), but for custom descriptors (User Description 0x2901, Presentation Format 0x2904) readDescriptor() is the only way to obtain metadata.
MTU and reading large data — if the characteristic value exceeds MTU (23 bytes for BLE 4.0), Android automatically fragments and reassembles data through a sequence of read requests via the BLE stack. For BLE 5.0+ with extended MTU (up to 251 bytes), fragmentation is not required — a single read returns complete data. Before reading, you can call requestMtu() to negotiate the maximum MTU.
// Read characteristic and descriptor via BluetoothGatt
class GattReader {
fun readHeartRate(gatt: BluetoothGatt) {
// Heart Rate Service UUID = 0x180D
val service = gatt.getService(UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
?: return
// Heart Rate Measurement UUID = 0x2A37
val characteristic = service.getCharacteristic(
UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB")
) ?: return
if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {
gatt.readCharacteristic(characteristic)
}
}
fun readDescriptor(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// CCCD (0x2902)
val cccd = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
)
cccd?.let { gatt.readDescriptor(it) }
}
// Process data in onCharacteristicRead callback:
fun parseHeartRate(value: ByteArray): Int {
// BLE Heart Rate: bytes = flags, second = bpm
return if (value.isNotEmpty()) value[1].toInt() and 0xFF else 0
}
private fun hasProperty(char: BluetoothGattCharacteristic, prop: Int): Boolean {
return char.properties and prop != 0
}
}
The GattReader class demonstrates reading a Heart Rate characteristic. Service 0x180D contains characteristic 0x2A37 (Heart Rate Measurement) — a standard Bluetooth SIG BLE profile. Before reading, the PROPERTY_READ property is checked via hasProperty. parseHeartRate parses the BLE heart rate format: first byte — flags (data format), second — bpm value. The CCCD descriptor (0x2902) is read to check notification status.
writeCharacteristic() is a BluetoothGatt method for writing data to a BLE peripheral. On Android API 33+, writeCharacteristic() has been replaced with writeCharacteristic(request), where BluetoothGattCharacteristicWriteRequest is a request object containing the characteristic, byte array, and WriteType. The old writeCharacteristic(characteristic) method with setValue() is deprecated. WriteType determines the request behavior: WRITE_TYPE_DEFAULT (depends on characteristic properties), WRITE_TYPE_NO_RESPONSE (without response), and WRITE_TYPE_SIGNED (authorization).
Choosing the WriteType affects speed and reliability. WRITE_TYPE_DEFAULT usually corresponds to withResponse (if the characteristic has PROPERTY_WRITE) or withoutResponse (if it has PROPERTY_WRITE_NO_RESPONSE). For streaming data (OTA updates, logs), use WRITE_TYPE_NO_RESPONSE — maximum throughput. For commands with delivery guarantee (enable, configure) — WRITE_TYPE_DEFAULT with confirmation via onCharacteristicWrite.
// BLE characteristic write on Android API 33+
class GattWriter {
// Write with response (withResponse)
fun writeWithResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// API 33+: writeCharacteristic
val request = BluetoothGattCharacteristicWriteRequest(
characteristic,
data,
BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_DEFAULT,
@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
)
gatt.writeCharacteristic(request)
} else {
// API < 33: (deprecated)
characteristic.setValue(data)
gatt.writeCharacteristic(characteristic)
}
}
// Write without response (max speed)
fun writeWithoutResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val request = BluetoothGattCharacteristicWriteRequest(
characteristic,
data,
BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_NO_RESPONSE,
@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
)
gatt.writeCharacteristic(request)
} else {
characteristic.setValue(data)
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(characteristic)
}
}
// onCharacteristicWrite callback (API 33+)
private val writeCallback = object : BluetoothGattCallback() {
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int,
callbackType: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
print("Write success: ${value.size} bytes")
}
}
}
}
The GattWriter class supports both WriteTypes for different API levels. writeWithResponse uses WRITE_TYPE_DEFAULT — the BLE device confirms the write via onCharacteristicWrite. writeWithoutResponse uses WRITE_TYPE_NO_RESPONSE — data is sent without confirmation, maximum throughput. On API 33+, the new writeCharacteristic(request) with BluetoothGattCharacteristicWriteRequest is used. On API < 33 — the old setValue() + writeCharacteristic().
setCharacteristicNotification() is a BluetoothGatt method for subscribing to notifications about characteristic changes on the peripheral. After activating the subscription, the BLE device sends new values via onCharacteristicChanged(). However, setCharacteristicNotification() only activates the local Android notification — to enable notifications on the BLE device itself, you must also write the value 0x0100 to the CCCD descriptor (0x2902).
CCCD (Client Characteristic Configuration Descriptor) is a descriptor that controls sending notifications from the BLE peripheral. Value 0x0000 — notifications disabled, 0x0100 — notifications enabled, 0x0200 — indications enabled. Writing to CCCD is done via writeDescriptor() on BluetoothGatt after calling setCharacteristicNotification(). Android does not write CCCD automatically — this responsibility lies with the developer.
// Correct BLE notification subscription
class GattNotificationManager {
// 1. Enable notifications
fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// Step 1: local Android subscription
val success = gatt.setCharacteristicNotification(characteristic, true)
if (!success) {
print("Failed to subscribe")
return
}
// Step 2: write CCCD (0x2902) on BLE device
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
// 0x0100 = notification, 0x0200 = indicate
val cccdValue = if (characteristic.properties
and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) {
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE // [0x01, 0x00]
} else {
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE // [0x02, 0x00]
}
cccdDescriptor.setValue(cccdValue)
gatt.writeDescriptor(cccdDescriptor)
}
// 2. Characteristic discovery
fun disableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
gatt.setCharacteristicNotification(characteristic, false)
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
cccdDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
gatt.writeDescriptor(cccdDescriptor)
}
// 3. Notification callback
private val notificationCallback = object : BluetoothGattCallback() {
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
callbackType: Int
) {
// New value from BLE peripheral
print("Notification: ${value.size} bytes")
}
}
}
The GattNotificationManager class implements the correct two-step BLE notification subscription protocol. enableNotification first calls setCharacteristicNotification(true) on Android, then writes 0x0100 to the CCCD descriptor via writeDescriptor. disableNotification performs the reverse operations. Without writing CCCD, the BLE device does not send notifications — this is the most common mistake among BLE developers on Android.
Complete example of a GATT client in Kotlin that combines BluetoothGatt creation, discovery, reading, and notification subscription in a single manager using coroutines for asynchronous processing.
// Full GATT client with coroutines in Kotlin
class GattClient(context: Context) {
private val context = context.applicationContext
private var gatt: BluetoothGatt? = null
// Connect with coroutine
suspend fun connect(device: BluetoothDevice): Boolean =
suspendCoroutine { continuation ->
gatt = device.connectGatt(
context, false,
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
} else {
continuation.resume(false)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
continuation.resume(status == BluetoothGatt.GATT_SUCCESS)
}
},
BluetoothDevice.TRANSPORT_LE
)
}
// Read characteristic via coroutine
suspend fun readCharacteristicValue(char: BluetoothGattCharacteristic): ByteArray? =
suspendCoroutine { continuation ->
gatt?.let { gatt ->
// Save characteristic tag for callback identification
gatt.setCharacteristic(char, null) // for API < 33
gatt.readCharacteristic(char)
}
}
// Close connection
fun release() {
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
The GattClient GATT client uses coroutines (suspendCoroutine) to transform the callback-based BluetoothGatt API into sequential calls. connect() waits for onServicesDiscovered, after which the GATT hierarchy is available. readCharacteristicValue() waits for onCharacteristicRead. This approach eliminates callback nesting and makes BLE code linear. release() guarantees resource cleanup — a mandatory call in Activity onDestroy or ViewModel.onCleared.
Frequently Asked Questions
BluetoothGatt is an Android class for GATT client that manages a BLE connection with a peripheral device. It is created via BluetoothDevice.connectGatt(), providing methods discoverServices(), readCharacteristic(), writeCharacteristic(), setCharacteristicNotification(). Results of all operations arrive asynchronously via BluetoothGattCallback. Without BluetoothGatt, bidirectional BLE communication on Android is impossible.
Status 133 (GATT_ERROR) indicates an internal Android BLE stack error. Causes: the device disconnected during discovery, MTU is below minimum (23 bytes), or the BLE stack is overloaded. Solution: retry discoverServices() with a 500ms delay, check the device RSSI, and ensure the peripheral supports GATT discovery in its current state.
For writing with confirmation, call writeCharacteristic() with WRITE_TYPE_DEFAULT (API 33+: BluetoothGattCharacteristicWriteRequest). On success, the BLE device sends a confirmation, and Android calls onCharacteristicWrite with GATT_SUCCESS. If the device does not respond within 30 seconds (stack timeout), the callback returns an error status. For watchdog, use Handler with postDelayed.
On Android 13+ (API 33), BluetoothGatt methods have changed: writeCharacteristic() now accepts BluetoothGattCharacteristicWriteRequest, readCharacteristic() — BluetoothGattCharacteristicReadRequest. The old setValue()/writeCharacteristic() are deprecated. BluetoothGattCallback has also changed: onCharacteristicRead(), onCharacteristicWrite(), onCharacteristicChanged() receive ByteArray value and callbackType. Use Build.VERSION.SDK_INT for branching.
Android supports 4–8 simultaneous BLE GATT connections (varies by manufacturer and Android version). Pixel/Google: up to 7, Samsung: up to 5, Xiaomi: up to 4. When the limit is exceeded, connectGatt returns null or onConnectionStateChange with an error. For working with a large number of devices, use cyclic connection or Bluetooth Mesh.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also