BluetoothGatt — Android-klasse die API biedt voor de werking van GATT-client (Generic Attribute Profile) over BLE-verbinding. BluetoothGatt kapselt de verbinding met een externe GATT-server (perifeer BLE-apparaat) in en beheert alle profielbewerkingen: ontdekken van services, lezen en schrijven van kenmerken, abonneren op meldingen en indicaties. Een BluetoothGatt-instantie wordt verkregen via BluetoothDevice.connectGatt() met callback BluetoothGattCallback. Volgens Android Developers, 2026 is BluetoothGatt de centrale klasse voor bidirectionele BLE-communicatie, die GATT-bewerkingen ondersteunt van BLE 4.0 tot BLE 5.4.
Belangrijkste punten
BluetoothGatt — is een proxy-object dat de GATT-verbinding vertegenwoordigt tussen een Android-apparaat (centraal) en BLE-periferie (server). Elke BluetoothGatt-instantie komt overeen met één actieve BLE-verbinding. Via deze worden alle GATT-profielbewerkingen uitgevoerd: ontdekken, lezen, schrijven, meldingen. BluetoothGatt wordt niet direct gemaakt — het wordt geretourneerd door de methode BluetoothDevice.connectGatt().
Het maken van BluetoothGatt vereist vier parameters. Context — applicatiecontext (Activity of Application). autoConnect — indien false, start Android onmiddellijk een directe verbinding; indien true, maakt Android automatisch verbinding bij detectie van het apparaat (nuttig voor achtergrondverbinding). BluetoothGattCallback — verplichte callback voor alle GATT-gebeurtenissen. transport — BluetoothDevice.TRANSPORT_LE (BLE) of TRANSPORT_BREDR (Classic). Gebruik op BLE-apparaten altijd TRANSPORT_LE.
Levenscyclus van BluetoothGatt bestaat uit vijf toestanden. DISCONNECTED — begintoestand. CONNECTING — na aanroep van connectGatt, tot bevestiging. CONNECTED — na onConnectionStateChange met STATE_CONNECTED. Na verbinding wordt discoverServices() aangeroepen om de GATT-hiërarchie te verkrijgen. Na voltooiing — disconnect() en close() om systeembronnen vrij te geven. Zonder close() kan de app de Android BLE-verbindingslimiet uitputten (meestal 4–8).
// BluetoothGatt-verbinding maken
import android.bluetooth.*
class GattConnector(private val context: Context) {
private var bluetoothGatt: BluetoothGatt? = null
fun connect(device: BluetoothDevice): BluetoothGatt? {
// Sluit vorige verbinding indien aanwezig
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. Verbinding tot stand gebracht → Ontdekking
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
// 3. Verbinding verbroken
close()
}
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt, status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 4. GATT-hiërarchie ontvangen
onGattReady(gatt)
}
}
},
BluetoothDevice.TRANSPORT_LE
)
return bluetoothGatt
}
private fun onGattReady(gatt: BluetoothGatt) {
// GATT gereed voor lees-/schrijfbewerkingen
}
fun close() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
}
}
De klasse GattConnector demonstreert correct aanmaken van BluetoothGatt. Parameter autoConnect=false — directe verbinding (voor gescande apparaten). Bij onConnectionStateChange met STATE_CONNECTED wordt onmiddellijk discoverServices() aangeroepen. onServicesDiscovered geeft GATT-gereedheid aan. close() roept achtereenvolgens disconnect() en close() aan — zonder close() worden systeembronnen niet vrijgegeven, wat leidt tot lekkage van BLE-verbindingen.
discoverServices() — eerste GATT-methode aangeroepen na verbinding. Start asynchrone zoektocht naar alle services op BLE-periferie. Resultaat komt in onServicesDiscovered() met statuscode: GATT_SUCCESS (0) — succes, 133 — GATT_ERROR, 8 — GATT_CONNECTION_TIMEOUT. Na succesvolle ontdekking vult BluetoothGatt de lijst met services die beschikbaar zijn via getServices().
Elke BluetoothGattService bevat een lijst van BluetoothGattCharacteristic. Een kenmerk heeft UUID, eigenschappen (PROPERTY_READ, PROPERTY_WRITE, PROPERTY_NOTIFY) en optionele descriptoren. Eigenschappen bepalen welke bewerkingen zijn toegestaan: als een kenmerk geen PROPERTY_READ heeft, retourneert aanroep van readCharacteristic een fout. Voor het verkrijgen van descriptors van een kenmerk wordt getDescriptors() gebruikt.
// Service-ontdekking en kenmerkzoekopdracht
class GattServiceExplorer {
// Vind service op UUID
fun findService(gatt: BluetoothGatt, uuid: UUID): BluetoothGattService? {
return gatt.services?.firstOrNull { it.uuid == uuid }
}
// Vind kenmerk in service
fun findCharacteristic(
service: BluetoothGattService,
uuid: UUID
): BluetoothGattCharacteristic? {
return service.characteristics?.firstOrNull { it.uuid == uuid }
}
// Verkrijg alle ondersteunde kenmerkbebewerkingen
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 volledige GATT-hiërarchie
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}")
}
}
}
}
}
De klasse GattServiceExplorer biedt hulpprogramma's voor navigatie door de GATT-hiërarchie. findService en findCharacteristic zoeken services en kenmerken op UUID. getCharacteristicProperties controleert bitmaskers van eigenschappen via and(). dumpGattTree geeft de volledige hiërarchie weer in het log — handig bij foutopsporing van BLE-apparaten. Alle bewerkingen op BluetoothGatt moeten worden uitgevoerd na succesvolle onServicesDiscovered, anders retourneert getServices() een lege lijst.
readCharacteristic() — asynchrone BluetoothGatt-methode voor het lezen van de waarde van een kenmerk van een extern BLE-apparaat. Resultaat komt in onCharacteristicRead() van BluetoothGattCallback. Als er 2+ overeenkomende kenmerken op het apparaat zijn (onwaarschijnlijk maar mogelijk), kan readCharacteristic() een niet-doelwit lezen — veiliger is om readCharacteristic() op de BluetoothGattCharacteristic-instantie aan te roepen, niet op UUID.
readDescriptor() — methode voor het lezen van de waarde van een descriptorkenmerk. Een typische descriptor — CCCD (Client Characteristic Configuration Descriptor, UUID 0x2902) die bepaalt of meldingen zijn ingeschakeld. Resultaat in onDescriptorRead(). Het lezen van descriptoren is zelden nodig in de praktijk — CCCD wordt beheerd door setCharacteristicNotification(), maar voor aangepaste descriptoren (User Description 0x2901, Presentation Format 0x2904) is readDescriptor() de enige manier om metadata te verkrijgen.
MTU en grote gegevens lezen — als de waarde van het kenmerk groter is dan MTU (23 bytes voor BLE 4.0), fragmenteert en assembleert Android automatisch gegevens via een reeks leesverzoeken door de BLE-stack. Voor BLE 5.0+ met uitgebreide MTU (tot 251 bytes) is fragmentatie niet nodig — één lezing retourneert volledige gegevens. Voor het lezen kan requestMtu() worden aangeroepen voor het afstemmen van maximale MTU.
// Kenmerk en descriptor lezen 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) }
}
// Verwerk gegevens in onCharacteristicRead-callback:
fun parseHeartRate(value: ByteArray): Int {
// BLE Heart Rate: bytes = flags, tweede = 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
}
}
De klasse GattReader demonstreert het lezen van de Heart Rate-kenmerk. Service 0x180D bevat kenmerk 0x2A37 (Heart Rate Measurement) — standaard BLE-profiel van Bluetooth SIG. Voor het lezen wordt eigenschap PROPERTY_READ gecontroleerd via hasProperty. parseHeartRate parseert het BLE-hartslagformaat: eerste byte — flags (gegevensformaat), tweede — bpm-waarde. CCCD-descriptor (0x2902) wordt gelezen om de meldingsstatus te controleren.
writeCharacteristic() — BluetoothGatt-methode voor het schrijven van gegevens naar BLE-periferie. Op Android API 33+ is writeCharacteristic() vervangen door writeCharacteristic(request), waarbij BluetoothGattCharacteristicWriteRequest een verzoekobject is dat het kenmerk, een byte-array en WriteType bevat. De oude methode writeCharacteristic(characteristic) met setValue() is verouderd. WriteType bepaalt het gedrag van het verzoek: WRITE_TYPE_DEFAULT (afhankelijk van kenmerkeigenschappen), WRITE_TYPE_NO_RESPONSE (withoutResponse) en WRITE_TYPE_SIGNED (autorisatie).
Keuze van WriteType beïnvloedt snelheid en betrouwbaarheid. WRITE_TYPE_DEFAULT komt meestal overeen met withResponse (als het kenmerk PROPERTY_WRITE heeft) of withoutResponse (als het PROPERTY_WRITE_NO_RESPONSE heeft). Voor streamgegevens (OTA-updates, logs) gebruik WRITE_TYPE_NO_RESPONSE — maximale bandbreedte. Voor opdrachten met leveringsgarantie (inschakelen, configuratie) — WRITE_TYPE_DEFAULT met bevestiging via onCharacteristicWrite.
// BLE-kenmerk schrijven op Android API 33+
class GattWriter {
// Schrijven met antwoord (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)
}
}
// Schrijven zonder antwoord (maximale snelheid)
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")
}
}
}
}
De klasse GattWriter ondersteunt beide WriteTypes voor verschillende API-niveaus. writeWithResponse gebruikt WRITE_TYPE_DEFAULT — BLE-apparaat bevestigt schrijven via onCharacteristicWrite. writeWithoutResponse gebruikt WRITE_TYPE_NO_RESPONSE — gegevens worden zonder bevestiging verzonden, maximale bandbreedte. Op API 33+ wordt de nieuwe writeCharacteristic(request) met BluetoothGattCharacteristicWriteRequest gebruikt. Op API < 33 — oude setValue() + writeCharacteristic().
setCharacteristicNotification() — BluetoothGatt-methode voor abonneren op meldingen over verandering van kenmerk op de periferie. Na activering van het abonnement stuurt het BLE-apparaat nieuwe waarden via onCharacteristicChanged(). Echter activeert setCharacteristicNotification() alleen de lokale Android-melding — om meldingen op het BLE-apparaat zelf in te schakelen, moet ook waarde 0x0100 in de CCCD-descriptor (0x2902) worden geschreven.
CCCD (Client Characteristic Configuration Descriptor) — descriptor die het verzenden van meldingen van BLE-periferie beheert. Waarde 0x0000 — meldingen uitgeschakeld, 0x0100 — meldingen ingeschakeld (notifications), 0x0200 — indicaties ingeschakeld (indications). Schrijven naar CCCD gebeurt via writeDescriptor() op BluetoothGatt na aanroep van setCharacteristicNotification(). Android schrijft CCCD niet automatisch — deze verantwoordelijkheid ligt bij de ontwikkelaar.
// Correcte BLE-meldingsabonnement
class GattNotificationManager {
// 1. Meldingen inschakelen
fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// Stap 1: lokale Android-abonnement
val success = gatt.setCharacteristicNotification(characteristic, true)
if (!success) {
print("Abonneren mislukt")
return
}
// Stap 2: CCCD (0x2902) schrijven op BLE-apparaat
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
// 0x0100 = melding, 0x0200 = indicatie
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. Kenmerkontdekking
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. Meldingscallback
private val notificationCallback = object : BluetoothGattCallback() {
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
callbackType: Int
) {
// Nieuwe waarde van BLE-periferie
print("Notification: ${value.size} bytes")
}
}
}
De klasse GattNotificationManager implementeert het juiste tweetrapsprotocol voor abonneren op BLE-meldingen. enableNotification roept eerst setCharacteristicNotification(true) aan op Android, schrijft vervolgens 0x0100 naar de CCCD-descriptor via writeDescriptor. disableNotification voert de omgekeerde bewerkingen uit. Zonder CCCD-schrijven stuurt het BLE-apparaat geen meldingen — dit is de meest voorkomende fout van BLE-ontwikkelaars op Android.
Volledig voorbeeld van een GATT-client in Kotlin die het aanmaken van BluetoothGatt, ontdekken, lezen en abonneren op meldingen combineert in één manager met behulp van coroutines voor asynchrone verwerking.
// Volledige GATT-client met coroutines in Kotlin
class GattClient(context: Context) {
private val context = context.applicationContext
private var gatt: BluetoothGatt? = null
// Verbinding maken met 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
)
}
// Kenmerk lezen via coroutine
suspend fun readCharacteristicValue(char: BluetoothGattCharacteristic): ByteArray? =
suspendCoroutine { continuation ->
gatt?.let { gatt ->
// Bewaar kenmerktag voor callback-identificatie
gatt.setCharacteristic(char, null) // voor API < 33
gatt.readCharacteristic(char)
}
}
// Verbinding sluiten
fun release() {
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
De GATT-client GattClient gebruikt coroutines (suspendCoroutine) om de callback-gebaseerde BluetoothGatt API om te zetten in sequentiële aanroepen. connect() wacht op onServicesDiscovered, waarna de GATT-hiërarchie beschikbaar is. readCharacteristicValue() wacht op onCharacteristicRead. Deze benadering elimineert geneste callbacks en maakt BLE-code lineair. release() garandeert het vrijgeven van bronnen — verplichte aanroep in onDestroy van Activity of ViewModel.onCleared.
Veelgestelde vragen
BluetoothGatt — klasse voor Android GATT-client die BLE-verbinding met een perifeer apparaat beheert. Wordt gemaakt via BluetoothDevice.connectGatt(), biedt methoden discoverServices(), readCharacteristic(), writeCharacteristic(), setCharacteristicNotification(). Resultaten van alle bewerkingen komen asynchroon via BluetoothGattCallback. Zonder BluetoothGatt is bidirectionele BLE-communicatie op Android onmogelijk.
Status 133 (GATT_ERROR) betekent een interne fout van de Android BLE-stack. Oorzaken: apparaat is verbroken tijdens ontdekking, MTU is kleiner dan minimaal (23 bytes), of de BLE-stack is overbelast. Oplossing: herhaal discoverServices() met een vertraging van 500 ms, controleer de RSSI van het apparaat en zorg dat de periferie GATT-ontdekking ondersteunt in de huidige staat.
Voor schrijven met bevestiging roept u writeCharacteristic() aan met WRITE_TYPE_DEFAULT (API 33+: BluetoothGattCharacteristicWriteRequest). Bij succes stuurt het BLE-apparaat een bevestiging en Android roept onCharacteristicWrite aan met GATT_SUCCESS. Als het apparaat niet reageert binnen 30 seconden (stack-timeout), retourneert de callback een foutstatus. Gebruik voor watchdog Handler met postDelayed.
Op Android 13+ (API 33) zijn BluetoothGatt-methoden gewijzigd: writeCharacteristic() accepteert nu BluetoothGattCharacteristicWriteRequest, readCharacteristic() — BluetoothGattCharacteristicReadRequest. Oude setValue()/writeCharacteristic() zijn verouderd. Ook BluetoothGattCallback is gewijzigd: onCharacteristicRead(), onCharacteristicWrite(), onCharacteristicChanged() ontvangen ByteArray value en callbackType. Gebruik Build.VERSION.SDK_INT voor vertakking.
Android ondersteunt 4–8 gelijktijdige BLE-GATT-verbindingen (afhankelijk van fabrikant en Android-versie). Pixel/Google: tot 7, Samsung: tot 5, Xiaomi: tot 4. Bij overschrijding van de limiet retourneert connectGatt null of onConnectionStateChange met fout. Voor werken met veel apparaten gebruikt u cyclische verbinding of Bluetooth Mesh.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook