Each ScanResult contains:
FlutterBluePlus.scanResults.listen((results) {
for (ScanResult r in results) {
// The device object for connecting
BluetoothDevice device = r.device;
// Signal strength (-40 = excellent, -80 = weak)
int rssi = r.rssi;
// Advertising data from the peripheral
AdvertisementData adData = r.advertisementData;
// Device name from advertisement (may differ from bonded name)
String? localName = adData.localName;
// Service UUIDs the device advertises
List<String> serviceUuids = adData.serviceUuids;
// Manufacturer data (company-specific)
Map<int, List<int>> manufacturerData = adData.manufacturerData;
// TX power level (for distance estimation)
int? txPower = adData.txPowerLevel;
// Whether the device appears for the first time this scan
bool isNew = !seenDevices.contains(device.remoteId);
}
});
Filtering Scan Results
Filter by Service UUID (Most Reliable)
The most reliable way to find your device is to filter by the service UUID your device advertises. This works even if the device name is blank:
const String TARGET_SERVICE_UUID = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E';
await FlutterBluePlus.startScan(
withServices: [Guid(TARGET_SERVICE_UUID)],
timeout: const Duration(seconds: 15),
);
FlutterBluePlus.scanResults.listen((results) {
// Only devices advertising our service UUID appear here
for (ScanResult r in results) {
print('Found our device: ${r.device.platformName}');
}
});
Filter by Device Name
FlutterBluePlus.scanResults.listen((results) {
for (ScanResult r in results) {
if (r.device.platformName.contains('MySensor')) {
print('Found target device!');
}
}
});
Filter by RSSI (Signal Strength)
Use RSSI filtering to show only nearby devices:
const int MIN_RSSI = -70; // Only show devices within ~5 meters
FlutterBluePlus.scanResults.listen((results) {
final nearbyDevices = results.where((r) => r.rssi >= MIN_RSSI).toList();
// Sort by signal strength (closest first)
nearbyDevices.sort((a, b) => b.rssi.compareTo(a.rssi));
for (ScanResult r in nearbyDevices) {
print('${r.device.platformName}: ${r.rssi} dBm');
}
});
Avoiding Duplicate Results
By default, scanResults emits a new list every time any device updates. Use removeDuplicates or manage state manually:
// Using withKeywords to avoid duplicates by device ID
final Map<DeviceIdentifier, ScanResult> _seen = {};
FlutterBluePlus.scanResults.listen((results) {
for (ScanResult r in results) {
_seen[r.device.remoteId] = r; // Overwrites with latest RSSI
}
// Now _seen.values contains deduplicated, up-to-date results
final uniqueDevices = _seen.values.toList();
setState(() => _devices = uniqueDevices);
});
Scan Parameters Deep Dive
await FlutterBluePlus.startScan(
// Only find devices advertising these service UUIDs
withServices: [Guid('180D')], // Heart Rate Service
// Only find devices with these names (Android only)
withNames: ['MySensor', 'MyDevice'],
// Scan timeout — always set this!
timeout: const Duration(seconds: 15),
// Remove duplicates from stream (convenience option)
// Note: set to false if you want RSSI updates
androidScanMode: AndroidScanMode.lowLatency, // or .lowPower for background
// Continue scan after first result (true = stop after finding one device)
oneByOne: false,
);
Android Scan Modes
| Mode |
Use Case |
Battery Impact |
lowLatency |
Active scanning in foreground |
High |
balanced |
Default — good for most cases |
Medium |
lowPower |
Background or long-running scans |
Low |
opportunistic |
Passive — only sees results from other apps |
Negligible |
Managing Scan State in Flutter
Here's a production-ready scan state pattern:
class BleScannerProvider extends ChangeNotifier {
final Map<DeviceIdentifier, ScanResult> _results = {};
bool _isScanning = false;
StreamSubscription? _scanSubscription;
StreamSubscription? _isScanningSubscription;
List<ScanResult> get devices => _results.values.toList()
..sort((a, b) => b.rssi.compareTo(a.rssi));
bool get isScanning => _isScanning;
BleScannerProvider() {
_isScanningSubscription = FlutterBluePlus.isScanning.listen((scanning) {
_isScanning = scanning;
notifyListeners();
});
}
Future<void> startScan() async {
_results.clear();
notifyListeners();
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
for (final r in results) {
if (r.device.platformName.isNotEmpty) {
_results[r.device.remoteId] = r;
}
}
notifyListeners();
});
await FlutterBluePlus.startScan(
timeout: const Duration(seconds: 10),
androidScanMode: AndroidScanMode.lowLatency,
);
}
Future<void> stopScan() async {
await FlutterBluePlus.stopScan();
await _scanSubscription?.cancel();
}
@override
void dispose() {
stopScan();
_isScanningSubscription?.cancel();
super.dispose();
}
}
Scanning UI Pattern
class ScanPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => BleScannerProvider(),
child: Consumer<BleScannerProvider>(
builder: (context, scanner, _) {
return Scaffold(
appBar: AppBar(
title: const Text('Find BLE Devices'),
actions: [
if (scanner.isScanning)
const Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 20, height: 20,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
),
),
],
),
body: ListView.builder(
itemCount: scanner.devices.length,
itemBuilder: (context, index) {
final result = scanner.devices[index];
return ListTile(
title: Text(result.device.platformName.isEmpty
? 'Unknown Device'
: result.device.platformName),
subtitle: Text(result.device.remoteId.toString()),
trailing: Text('${result.rssi} dBm'),
onTap: () => _connectToDevice(context, result.device),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: scanner.isScanning
? scanner.stopScan
: scanner.startScan,
label: Text(scanner.isScanning ? 'Stop' : 'Scan'),
icon: Icon(scanner.isScanning ? Icons.stop : Icons.search),
),
);
},
),
);
}
}
iOS-Specific Scanning Considerations
iOS has strict background scanning limitations:
- App must be in foreground for full scanning — use Core Location for background
- Service UUID filter required for background scanning — without it iOS won't deliver results
- Device names may be cached — iOS caches names from previous connections, not always from advertisement
- State preservation/restoration — needed for background scanning approval
// iOS: Always use service UUID filter for consistent results
await FlutterBluePlus.startScan(
withServices: [Guid(YOUR_SERVICE_UUID)], // Required for iOS background
timeout: const Duration(seconds: 30),
);
Android 12+ Scanning Permissions
On Android 12+, scanning requires BLUETOOTH_SCAN permission. See the complete Android & iOS permissions guide for the full setup.
// Check and request permissions before scanning
Future<bool> checkPermissions() async {
if (Platform.isAndroid) {
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.location,
].request();
return statuses.values.every((s) => s.isGranted);
} else if (Platform.isIOS) {
// iOS permissions handled via Info.plist + system dialog
return true;
}
return false;
}
Battery-Efficient Scanning
Continuous scanning kills battery. Use these patterns:
// Pattern 1: Scan for fixed duration
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));
// Auto-stops after 10 seconds
// Pattern 2: Stop immediately when target found
late StreamSubscription _sub;
_sub = FlutterBluePlus.scanResults.listen((results) async {
for (final r in results) {
if (r.device.platformName == 'MyDevice') {
await FlutterBluePlus.stopScan();
await _sub.cancel();
connectToDevice(r.device);
break;
}
}
});
// Pattern 3: Low-power mode for long searches
await FlutterBluePlus.startScan(
androidScanMode: AndroidScanMode.lowPower,
timeout: const Duration(minutes: 1),
);
Connecting After Scanning
Always stop scanning before connecting — scanning consumes radio resources needed for connection:
Future<void> connectToDevice(BluetoothDevice device) async {
// Step 1: Stop scanning
await FlutterBluePlus.stopScan();
// Step 2: Connect
await device.connect(
timeout: const Duration(seconds: 15),
autoConnect: false,
);
// Step 3: Discover services
List<BluetoothService> services = await device.discoverServices();
// Step 4: Start interacting with GATT
// See: /blog/ble-gatt-profiles-explained
}
Related Guides
Frequently Asked Questions
Why does my Flutter BLE scan return no results?
Common causes: (1) Bluetooth permissions not granted — check Android 12+ BLUETOOTH_SCAN permission, (2) Bluetooth adapter is off, (3) filtering by a service UUID the device doesn't advertise, (4) on iOS, device name may not appear until after first connection. Always check FlutterBluePlus.adapterState first.
How do I scan for BLE devices in the background on Flutter?
Background scanning is platform-limited. On iOS, you must use service UUID filtering and register for Core Bluetooth state restoration. On Android, use a Foreground Service with androidScanMode: AndroidScanMode.lowPower. flutter_blue_plus alone doesn't handle background scanning — you need platform channel code.
Why does BLE scanning drain battery so fast?
The BLE radio actively listens on all advertising channels (37, 38, 39) during a scan. Always set a timeout, use low-power scan mode when possible, and stop scanning the moment you've found your device. Avoid continuous/infinite scans.
Can I scan for specific BLE device names in Flutter?
Yes. On Android, you can use the withNames parameter in startScan(). On iOS, device names are not always in the advertisement packet — you may need to connect first to get the full name. Filter by service UUID on iOS for reliability.
How do I show signal strength (distance) in my Flutter BLE scan UI?
Use the r.rssi value from ScanResult. RSSI of -40 to -55 dBm is very close (< 1m), -55 to -70 dBm is close (1–5m), -70 to -85 dBm is moderate (5–15m), below -85 dBm is far. Exact distance calculation requires knowing the TX power level.
What's the fastest way to learn BLE scanning and beyond?
The BLE Flutter Course walks you through every BLE scanning pattern shown in this article, plus connecting, GATT, notifications, and building complete projects with real hardware.
Summary
BLE scanning in Flutter is straightforward with flutter_blue_plus, but production apps need careful attention to permissions, filtering, battery management, and iOS quirks. The patterns above handle all the edge cases you'll encounter.
Ready for the next step? Now that you can find and connect to devices, learn how to read and write BLE characteristics and interact with GATT services.
Or dive into the BLE Flutter Course for structured learning with real hardware projects.
👉 Enroll in the BLE Flutter Course →