Ble Scanner
A Bluetooth Low Energy (BLE) scanner that provides reactive scanning capabilities using Kotlin Flows.
This class wraps the Android BluetoothLeScanner API and exposes BLE scanning functionality through a coroutine-based Flow interface, making it easy to collect scan results in a lifecycle-aware manner. It also supports background scanning using android.app.PendingIntent for scenarios where the app needs to receive scan results even when not in the foreground.
Permissions
Before using this scanner, ensure the following permissions are granted:
android.permission.BLUETOOTH_SCAN(Android 12+)android.permission.BLUETOOTHandandroid.permission.BLUETOOTH_ADMIN(Android 11 and below)android.permission.ACCESS_FINE_LOCATION(required for BLE scanning on most Android versions)
Flow Lifecycle
The scanDevices method returns a cold Flow that:
Starts scanning when the Flow is collected
Emits ScanResult objects as BLE devices are discovered
Automatically stops scanning when the collector cancels or the coroutine scope is cancelled
Handles cleanup via awaitClose to ensure the scan is properly stopped
This makes it inherently lifecycle-safe when used with lifecycle-aware coroutine scopes such as viewModelScope or lifecycleScope.
Usage Examples
Basic Scanning
val bleScanner = BleScanner(context, bluetoothAdapter)
// Collect scan results in a coroutine scope
viewModelScope.launch {
bleScanner.scanDevices()
.catch { e -> Log.e("BLE", "Scan error: ${e.message}") }
.collect { scanResult ->
val device = scanResult.device
Log.d("BLE", "Found device: ${device.name} - ${device.address}")
}
}Scanning with Custom Settings and Filters
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.setReportDelay(1000) // Batch results every 1 second
.build()
val scanFilters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb")))
.build()
)
viewModelScope.launch {
bleScanner.scanDevices(scanSettings, scanFilters)
.collect { scanResult ->
// Only receives devices matching the filter
}
}Timeout-Based Scanning
viewModelScope.launch {
withTimeoutOrNull(10_000) { // Scan for 10 seconds
bleScanner.scanDevices()
.collect { scanResult ->
processDevice(scanResult)
}
}
// Scan automatically stops after timeout
}Background Scanning
// Create a PendingIntent for your BroadcastReceiver
val intent = Intent(context, BleScanReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.build()
// Start background scan
bleScanner.startBackgroundScan(scanSettings, emptyList(), pendingIntent)
// Later, stop the background scan
bleScanner.stopBackgroundScan(pendingIntent)See also
Functions
Scans for nearby BLE devices and emits discovered devices through a reactive Flow.
Starts a background BLE scan that delivers results via a android.app.PendingIntent.
Stops an ongoing background BLE scan that was previously started with startBackgroundScan.