neverForLocation flag: Add this to BLUETOOTH_SCAN if your app does NOT use BLE scanning to derive the user's location. This exempts you from requiring location permission on Android 12+. If you remove it, you must also declare and request ACCESS_FINE_LOCATION.
2. Minimum SDK Version
// android/app/build.gradle
android {
defaultConfig {
minSdkVersion 21 // Minimum for BLE support
targetSdkVersion 34
}
}
3. Runtime Permission Requests (Android 12+)
Add permission_handler to your pubspec:
dependencies:
permission_handler: ^11.3.0
import 'package:permission_handler/permission_handler.dart';
Future<bool> requestBluetoothPermissions() async {
if (!Platform.isAndroid) return true;
// Check Android SDK version
final sdkInt = await _getAndroidSdkVersion();
List<Permission> permissions;
if (sdkInt >= 31) {
// Android 12+
permissions = [
Permission.bluetoothScan,
Permission.bluetoothConnect,
];
} else {
// Android < 12
permissions = [
Permission.bluetooth,
Permission.locationWhenInUse,
];
}
final statuses = await permissions.request();
return statuses.values.every((s) => s.isGranted);
}
Future<int> _getAndroidSdkVersion() async {
if (!Platform.isAndroid) return 0;
final info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
}
4. Check Bluetooth Adapter State
Always verify Bluetooth is enabled before scanning:
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
Future<bool> isBluetoothReady() async {
final state = await FlutterBluePlus.adapterState.first;
return state == BluetoothAdapterState.on;
}
// Listen for state changes (user toggles Bluetooth)
FlutterBluePlus.adapterState.listen((state) {
if (state == BluetoothAdapterState.on) {
print('Bluetooth is on — ready to scan');
} else {
print('Bluetooth is off: $state');
}
});
iOS Setup
1. Info.plist
<!-- ios/Runner/Info.plist -->
<dict>
<!-- Required for iOS 13+ — describes WHY you need Bluetooth -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to and communicate with BLE devices.</string>
<!-- Required for iOS < 13 -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to connect to BLE devices.</string>
<!-- Required if your app uses BLE in the background -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<!-- Add bluetooth-peripheral if acting as a peripheral -->
</array>
</dict>
App Store rejection warning: Vague usage descriptions like "App uses Bluetooth" can get your app rejected. Be specific: explain what device you connect to and what data you exchange.
2. iOS Permission Flow
iOS shows the Bluetooth permission dialog automatically when you first call FlutterBluePlus.startScan() or FlutterBluePlus.adapterState. You don't need permission_handler for iOS Bluetooth specifically — the system handles it.
// On iOS, just check adapter state — system will prompt for permission
FlutterBluePlus.adapterState.listen((state) {
switch (state) {
case BluetoothAdapterState.on:
// Good to go
break;
case BluetoothAdapterState.unauthorized:
// User denied permission — guide them to Settings
showPermissionDeniedDialog();
break;
case BluetoothAdapterState.off:
// Bluetooth disabled
break;
default:
break;
}
});
3. Handling Permission Denied on iOS
If the user denies Bluetooth permission, you can't re-request it — you must direct them to Settings:
void showPermissionDeniedDialog() {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Bluetooth Permission Required'),
content: const Text(
'Please enable Bluetooth access in Settings > Privacy > Bluetooth to use this app.'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => openAppSettings(), // from permission_handler
child: const Text('Open Settings'),
),
],
),
);
}
Cross-Platform Permission Check
A unified helper that handles both platforms:
class BlePermissionManager {
static Future<BlePermissionResult> checkAndRequest() async {
// 1. Check if BLE is supported
if (!await FlutterBluePlus.isSupported) {
return BlePermissionResult.notSupported;
}
// 2. Request runtime permissions on Android
if (Platform.isAndroid) {
final granted = await _requestAndroidPermissions();
if (!granted) return BlePermissionResult.denied;
}
// 3. Check adapter state
final state = await FlutterBluePlus.adapterState.first;
if (state == BluetoothAdapterState.unauthorized) {
return BlePermissionResult.denied;
}
if (state != BluetoothAdapterState.on) {
return BlePermissionResult.bluetoothOff;
}
return BlePermissionResult.ready;
}
static Future<bool> _requestAndroidPermissions() async {
final info = await DeviceInfoPlugin().androidInfo;
final sdk = info.version.sdkInt;
final permissions = sdk >= 31
? [Permission.bluetoothScan, Permission.bluetoothConnect]
: [Permission.bluetooth, Permission.locationWhenInUse];
final statuses = await permissions.request();
return statuses.values.every((s) => s.isGranted);
}
}
enum BlePermissionResult { ready, denied, bluetoothOff, notSupported }
// Usage before scanning
final result = await BlePermissionManager.checkAndRequest();
switch (result) {
case BlePermissionResult.ready:
await startScan();
break;
case BlePermissionResult.denied:
showPermissionDeniedUI();
break;
case BlePermissionResult.bluetoothOff:
showEnableBluetoothUI();
break;
case BlePermissionResult.notSupported:
showNotSupportedUI();
break;
}
Common Permission Errors and Fixes
| Symptom |
Cause |
Fix |
| Scan returns no results (no error) |
Missing BLUETOOTH_SCAN permission |
Add to manifest + request at runtime |
SecurityException on connect |
Missing BLUETOOTH_CONNECT |
Add to manifest + request at runtime |
| iOS: no permission dialog appears |
Missing plist entry |
Add NSBluetoothAlwaysUsageDescription |
iOS: state = unauthorized immediately |
App has no plist description |
Add plist entry and reinstall |
| Android: works on API 30, fails on API 31 |
Old permissions only |
Add new Android 12 permissions |
| Location permission required |
neverForLocation flag missing |
Add flag to BLUETOOTH_SCAN |
Background BLE on iOS
For apps that must receive BLE data when backgrounded (e.g., health monitors):
<!-- Info.plist: declare background mode -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
// iOS background scanning: MUST filter by service UUID
await FlutterBluePlus.startScan(
withServices: [Guid(YOUR_SERVICE_UUID)], // Required for iOS background
androidScanMode: AndroidScanMode.lowPower,
);
Apple reviews background Bluetooth usage carefully — your app description must justify it clearly.
Related Guides
Frequently Asked Questions
Do I need location permission for BLE scanning on Android 12+?
Not if you add the android:usesPermissionFlags="neverForLocation" flag to BLUETOOTH_SCAN. This tells Android your app doesn't use scanning to derive location. If you remove this flag, you'll need ACCESS_FINE_LOCATION at runtime.
Why does my BLE scan silently return nothing on Android?
Almost always a missing permission. Unlike crashes, permission violations in BLE often fail silently. Double check: (1) BLUETOOTH_SCAN in manifest, (2) runtime permission granted, (3) Bluetooth adapter is on, (4) FlutterBluePlus.adapterState returns on.
Does flutter_blue_plus handle iOS Bluetooth permission dialogs automatically?
Yes. The iOS system dialog fires automatically on first Bluetooth use. You just need the correct plist entries. Use FlutterBluePlus.adapterState to detect the unauthorized state if the user denies.
Can I use permission_handler for iOS Bluetooth permissions?
permission_handler has limited support for iOS Bluetooth — it can check status but can't re-request after denial (iOS restriction). For iOS, rely on FlutterBluePlus.adapterState to detect authorization state and direct denied users to Settings.
What happens if I forget to declare Bluetooth permissions in Info.plist?
Your app will crash on iOS the first time it tries to use Bluetooth with a message like "This app has crashed because it attempted to access privacy-sensitive data without a usage description." Always add the plist entries before testing on a real device.
Is there a shortcut to get all BLE permissions right the first time?
BLE Starter ships these permission flows ready-made for Android 12+ and iOS, including denied and Bluetooth-off screens, so you start from working code instead of debugging missing permissions.
Get Permissions Right Once, Build Forever
Bluetooth permissions are a one-time setup that unlocks every other BLE feature. With the manifest entries, plist keys, and runtime request code above, you'll never hit a permission-related scan failure again.
Next step: Start scanning for BLE devices or jump straight to the complete app build guide.