FlutterBLEscanningflutter_blue_plusBluetooth

Flutter BLE Scanning Guide: Discover & Filter BLE Devices with flutter_blue_plus

March 28, 2026·9 min read

TL;DR: BLE scanning in Flutter uses FlutterBluePlus.startScan() and FlutterBluePlus.scanResults stream. Always filter by service UUID or device name to avoid noise, stop scanning before connecting, handle Android/iOS permission differences, and implement a scan timeout. This guide covers every scanning pattern you'll need in production.

Flutter BLE Scanning Guide: Discover & Filter BLE Devices with flutter_blue_plus

Before you can read sensor data, send commands, or build any BLE feature, you need to find the device. BLE scanning sounds simple, but there are dozens of edge cases: permissions on Android 12+, background scanning limitations on iOS, battery drain from continuous scanning, duplicate results flooding your UI, and connecting at the right moment. This guide covers all of it.


Prerequisites

Make sure you've:

  1. Added flutter_blue_plus to your pubspec.yaml (see Flutter BLE packages comparison)
  2. Configured Bluetooth permissions for Android & iOS
  3. Understood the BLE fundamentals

Basic Scanning

import 'package:flutter_blue_plus/flutter_blue_plus.dart';

// Check if Bluetooth is available and on
BluetoothAdapterState state = await FlutterBluePlus.adapterState.first;
if (state != BluetoothAdapterState.on) {
  print('Bluetooth is off');
  return;
}

// Start scanning (10 second timeout)
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));

// Listen for scan results
FlutterBluePlus.scanResults.listen((results) {
  for (ScanResult r in results) {
    print('${r.device.platformName}: RSSI ${r.rssi}');
    print('Device ID: ${r.device.remoteId}');
  }
});

// Stop scanning manually
await FlutterBluePlus.stopScan();

Understanding ScanResult

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:

  1. App must be in foreground for full scanning — use Core Location for background
  2. Service UUID filter required for background scanning — without it iOS won't deliver results
  3. Device names may be cached — iOS caches names from previous connections, not always from advertisement
  4. 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: /posts/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 →

Full Course Available

Ready to master BLE with Flutter?

Go from zero to production-ready BLE apps. The complete course covers scanning, connecting, GATT communication, custom hardware, background processing, and deploying real BLE-powered Flutter apps.

Enroll in the Course