BleScanner

class BleScanner(context: Context, bluetoothAdapter: BluetoothAdapter)

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.BLUETOOTH and android.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

Constructors

Link copied to clipboard
constructor(context: Context, bluetoothAdapter: BluetoothAdapter)

Creates a new BleScanner instance.

Functions

Link copied to clipboard
fun scanDevices(scanSettings: ScanSettings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build(), scanFilters: List<ScanFilter> = emptyList()): Flow<ScanResult>

Scans for nearby BLE devices and emits discovered devices through a reactive Flow.

Link copied to clipboard
fun startBackgroundScan(scanSettings: ScanSettings, scanFilters: List<ScanFilter>, pendingIntent: PendingIntent)

Starts a background BLE scan that delivers results via a android.app.PendingIntent.

Link copied to clipboard
fun stopBackgroundScan(pendingIntent: PendingIntent)

Stops an ongoing background BLE scan that was previously started with startBackgroundScan.