Android BLE Development: From Scanning to Data Transfer
Building BLE apps with Android's BLE API
Android BLE Development
Android's BLE API is stable from API 21+ but has well-documented pitfalls around connection management, threading, and MTU negotiation. This guide covers production-grade patterns.
BluetoothLeScanner
Always filter by service ATT">UUID to avoid processing irrelevant advertising packets:
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val filter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E"))
.build()
bluetoothAdapter.bluetoothLeScanner.startScan(listOf(filter), settings, callback)
Use SCAN_MODE_LOW_POWER for background scanning. Android 12+ requires BLUETOOTH_SCAN with neverForLocation if scan results are not used for location.
GATT Callback and Threading
Android's GATT callbacks arrive on a Binder thread. Request MTU before service discovery, then chain operations through callbacks:
device.connectGatt(context, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) gatt.requestMtu(512)
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
gatt.discoverServices()
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt, char: BluetoothGattCharacteristic, value: ByteArray
) { /* API 33+ — value passed directly, no race */ }
})
Common Issues
| Issue | Cause | Fix |
|---|---|---|
STATUS_133 on connect |
Android stack bug | Retry with exponential backoff |
| Characteristic reads stale | API < 33 value handling | Use API 33 onCharacteristicChanged(gatt, char, ByteArray) |
| Scan stops after 30 min | Background restriction | Use foreground service |
| Bonding loop | Incorrect CCCD write order | Write CCCD after discovery completes |
Permissions (Android 12+)
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
Best Practices
- Single GATT queue: Serialize all GATT operations — parallel calls cause
STATUS_133 - Bonding: Store peripheral MAC; use
autoConnect=truefor bonded devices - Reconnection: Exponential backoff on disconnect (100 ms → 200 ms → ... → 30 s cap)
- Coroutines: Wrap callbacks with
suspendCancellableCoroutinefor clean async code
Use the GATT Profile Browser to verify service UUIDs during development. For iOS patterns, see iOS Core Bluetooth Guide.
자주 묻는 질문
Yes, our guides range from beginner introductions to advanced topics. Each guide indicates its difficulty level and prerequisites so you can find the right starting point.