Flutter Bluetooth Mastery: BLE vs Classic with BLoC (The Guide Everyone Wishes They Had) (Part 1)

Flutter Bluetooth Mastery: BLE vs Classic with BLoC (The Guide Everyone Wishes They Had) (Part 1)

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!πŸš€

Master Flutter Bluetooth: BLE vs Classic with BLoC. Complete guide with working code for iOS & Android production apps.

Flutter Terminology

Finally understand which Bluetooth to use and build production-ready Flutter apps with clean BLoC architecture

Stop wrestling with Bluetooth confusion β€” master BLE and Classic implementations with BLoC state management that actually scales

Hey friend! πŸ‘‹

Let me guess β€” you've been staring at Bluetooth documentation trying to figure out if you need BLE, BLT, or "Bluetooth Classic" for your Flutter app? And every tutorial either skips the important stuff or assumes you already know everything?

I feel you. I've been there, debugging Bluetooth connections at 2 AM, wondering why my app works on Android but crashes on iOS.

Today, I'm giving you the complete, no-BS guide to Bluetooth in Flutter with BLoC state management. We'll cover what BLE and Bluetooth Classic actually mean, when to use each, and build real working code that handles all the edge cases.

Ready to become the Bluetooth expert on your team? Let's go.

What the Heck is BLE, BLT, and Bluetooth Classic?

Let's clear up the confusion first. You might've heard different terms thrown around β€” here's what they actually mean.

Bluetooth Classic (BR/EDR)

This is the "original" Bluetooth you know from the early 2000s. It's what connects your wireless headphones, speakers, and car audio systems.

Technical name: BR/EDR (Basic Rate/Enhanced Data Rate)

Common names: Bluetooth Classic, Traditional Bluetooth, Standard Bluetooth

What it's built for:

  • Streaming audio to headphones and speakers
  • File transfers between devices
  • Wireless keyboards and mice
  • Gaming controllers
  • Printer connections
  • Car entertainment systems

Power consumption: High β€” your battery feels it

Data throughput: Fast (up to 3 Mbps)

Range: Up to 100 meters in open space

Pairing requirement: Yes β€” users must explicitly pair devices through system settings

Real-world example: When you connect your AirPods to listen to music, that's Bluetooth Classic doing the heavy lifting.

BLE (Bluetooth Low Energy)

This is the newer, smarter version introduced in Bluetooth 4.0 (2010). It's designed for devices that need to conserve battery.

Technical name: BLE, Bluetooth LE, Bluetooth Smart

What you might incorrectly call it: BLT (that's a sandwich!), Bluetooth Light, Low Power Bluetooth

What it's built for:

  • Fitness trackers (Fitbit, Apple Watch, Garmin)
  • Smart home devices (lights, locks, thermostats)
  • Health monitors (blood pressure, glucose meters)
  • Proximity beacons (iBeacon, retail tracking)
  • IoT sensors (temperature, humidity, motion)
  • Bike computers and cycling sensors
  • Pet trackers and smart collars

Power consumption: Extremely low β€” devices can run for months or years on a tiny battery

Data throughput: Moderate (up to 1 Mbps, but typically much less)

Range: Up to 100 meters, but typically 10–30 meters

Pairing requirement: Optional β€” can work in "advertising mode" without pairing

Real-world example: Your Fitbit tracking your steps and heart rate all day without draining its battery β€” that's BLE magic.

The "BLT" Confusion

If someone says "BLT" in a Bluetooth context, they probably mean BLE but got the acronym wrong. BLT is definitely not a Bluetooth standard (unless we're talking bacon, lettuce, and tomato πŸ₯“πŸ₯¬πŸ…).

Quick Decision Guide: Which One Do I Need?

Let me make this super simple for you.

Use Bluetooth Classic if:

  • You're building a music streaming app
  • You need to send large files quickly
  • You're connecting to traditional peripherals (printers, keyboards)
  • Audio quality and bandwidth matter more than battery life

Use BLE if:

  • You're reading sensor data (temperature, heart rate, motion)
  • Battery life is critical
  • You're building an IoT or smart home app
  • You need to broadcast data to multiple devices
  • You're working with modern fitness or health devices

Pro tip: Most modern smartphones support BOTH simultaneously. Your iPhone or Android device has two Bluetooth radios running at the same time β€” one for Classic, one for BLE.

The Flutter Bluetooth Package Landscape

Here's what you need to install based on your use case.

For BLE (Bluetooth Low Energy):

flutter_blue_plus β€” The gold standard, actively maintained, best community support

dependencies:
flutter_blue_plus: ^1.31.15

For Bluetooth Classic:

flutter_bluetooth_serial β€” Best package for Classic Bluetooth

dependencies:
flutter_bluetooth_serial: ^0.4.0

For our guide today:

We're focusing on BLE with flutter_blue_plus because it's more commonly needed and has better documentation. The BLoC patterns we build will work for Classic too with minimal changes.

Setting Up Your Project (The Right Way)

Step 1: Dependencies

# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_blue_plus: ^1.31.15
flutter_bloc: ^8.1.3
equatable: ^2.0.5
permission_handler: ^11.3.0

dev_dependencies:
flutter_lints: ^3.0.0
bloc_test: ^9.1.5

Why these packages:

  • flutter_blue_plus β€” Best BLE implementation for Flutter
  • flutter_bloc β€” Clean state management that scales
  • equatable β€” Makes state comparison dead simple
  • permission_handler β€” Critical for runtime permissions
  • bloc_test β€” Essential for testing your BLoCs

Run: flutter pub get

πŸ€– Android Configuration (Every Single Detail)

This is where most projects fail. Let me show you EXACTLY what you need.

1. AndroidManifest.xml (Complete Setup)

<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- ===== BLUETOOTH PERMISSIONS FOR ANDROID 11 AND BELOW ===== -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />

<!-- ===== BLUETOOTH PERMISSIONS FOR ANDROID 12+ (API 31+) ===== -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADVERTISE" />

<!-- ===== LOCATION PERMISSIONS (Required for BLE on Android 11 and below) ===== -->
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />

<!-- ===== HARDWARE FEATURES ===== -->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />

<application
android:label="YourApp"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">

<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

Critical explanations:

android:usesPermissionFlags="neverForLocation" β€” This tells Android 12+ that you're NOT using Bluetooth for location tracking. Without this, users see scary "location permission" requests when you just want Bluetooth.

android:maxSdkVersion="30" β€” Prevents old permissions from triggering on new Android versions where they're not needed.

android:required="true" β€” Your app won't install on devices without BLE hardware.

2. build.gradle Configuration

// android/app/build.gradle
android {
namespace "com.yourcompany.yourapp"
compileSdk 34 // MUST be 31+ for Android 12 permissions

defaultConfig {
applicationId "com.yourcompany.yourapp"
minSdk 21 // Minimum for stable BLE
targetSdk 34 // Always target latest
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
}
debug {
applicationIdSuffix ".debug"
}
}
}

dependencies {
implementation 'androidx.multidex:multidex:2.0.1'
}

Why these settings matter:

  • compileSdk 34 required for new Bluetooth permission model
  • minSdk 21 is minimum for reliable BLE support
  • targetSdk 34 required for new Play Store submissions (Google policy)

3. ProGuard Rules (Don't Skip This!)

# android/app/proguard-rules.pro

# Keep Flutter Blue Plus classes
-keep class com.boskokg.flutter_blue_plus.** { *; }
-dontwarn com.boskokg.flutter_blue_plus.**

# Keep Bluetooth classes
-keep class android.bluetooth.** { *; }
-keep class android.bluetooth.le.** { *; }

# Keep BLoC classes
-keep class * extends androidx.lifecycle.ViewModel { *; }
-keepclassmembers class * extends androidx.lifecycle.ViewModel {
<init>(...);
}

🍎 iOS Configuration (Apple's Strict Requirements)

Apple is picky about Bluetooth. Here's how to make them happy.

1. Info.plist (Complete Setup)

<!-- ios/Runner/Info.plist -->
<dict>
<!-- ===== BLUETOOTH USAGE DESCRIPTIONS (REQUIRED) ===== -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>We need Bluetooth to connect to your fitness tracker and sync your health data in real-time.</string>

<!-- For iOS 12 and below (still include this) -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>We need Bluetooth to connect to your fitness tracker and health devices.</string>

<!-- ===== BACKGROUND MODES (Only if you need background BLE) ===== -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>

<!-- ===== APP TRANSPORT SECURITY ===== -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
</dict>

<!-- ===== SUPPORTED ORIENTATIONS ===== -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>

<key>CFBundleName</key>
<string>YourApp</string>
</dict>

Apple App Store REJECTION reasons:

❌ Generic description like "This app uses Bluetooth" β€” Apple wants to know WHY

βœ… Specific description like "Connect to your fitness tracker to sync health data"

❌ Requesting background modes you don't use β€” Apple tests this

βœ… Only include background modes if you genuinely need them

2. Podfile Setup

# ios/Podfile
platform :ios, '13.0' # Minimum for modern BLE

target 'Runner' do
use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)

target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
```

Run: `cd ios && pod install && cd ..`

---

### **3. iOS-Specific Critical Notes**

**iOS Simulator CANNOT test Bluetooth** β€” Always use real iPhone/iPad

**Background BLE is limited** β€” iOS kills connections after ~10 seconds unless properly configured

**CoreBluetooth permission timing** β€” Request only when needed, not on app startup

**App Store review** β€” Testers WILL connect real Bluetooth devices to verify your usage description

---

## **Building the BLoC Architecture**

Now let's build clean, scalable Bluetooth functionality with BLoC.

### **Project Structure**
```
lib/
β”œβ”€β”€ features/
β”‚ └── bluetooth/
β”‚ β”œβ”€β”€ data/
β”‚ β”‚ β”œβ”€β”€ models/
β”‚ β”‚ β”‚ └── ble_device.dart
β”‚ β”‚ └── repositories/
β”‚ β”‚ └── bluetooth_repository.dart
β”‚ β”œβ”€β”€ domain/
β”‚ β”‚ └── usecases/
β”‚ β”‚ β”œβ”€β”€ scan_devices_usecase.dart
β”‚ β”‚ └── connect_device_usecase.dart
β”‚ └── presentation/
β”‚ β”œβ”€β”€ bloc/
β”‚ β”‚ β”œβ”€β”€ bluetooth_bloc.dart
β”‚ β”‚ β”œβ”€β”€ bluetooth_event.dart
β”‚ β”‚ └── bluetooth_state.dart
β”‚ β”œβ”€β”€ screens/
β”‚ β”‚ β”œβ”€β”€ device_scan_screen.dart
β”‚ β”‚ └── device_detail_screen.dart
β”‚ └── widgets/
β”‚ └── device_tile.dart
└── main.dart

Step-by-Step Implementation

1. Create the BLE Device Model

// features/bluetooth/data/models/ble_device.dart
import 'package:equatable/equatable.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

class BleDevice extends Equatable {
final String id;
final String name;
final int rssi;
final BluetoothDevice device;

const BleDevice({
required this.id,
required this.name,
required this.rssi,
required this.device,
});

factory BleDevice.fromScanResult(ScanResult result) {
return BleDevice(
id: result.device.remoteId.str,
name: result.device.platformName.isEmpty
? 'Unknown Device'
: result.device.platformName,
rssi: result.rssi,
device: result.device,
);
}

// Signal strength interpretation
String get signalQuality {
if (rssi >= -50) return 'Excellent';
if (rssi >= -60) return 'Good';
if (rssi >= -70) return 'Fair';
return 'Poor';
}

// Approximate distance estimation
String get estimatedDistance {
final distance = pow(10, (-59 - rssi) / (10 * 2));
if (distance < 1) {
return '${(distance * 100).toStringAsFixed(0)} cm';
}
return '${distance.toStringAsFixed(1)} m';
}

@override
List<Object?> get props => [id, name, rssi];
}

Why Equatable? It makes comparing states in BLoC automatic. No more manual == operator overrides.

2. Build the Repository

// features/bluetooth/data/repositories/bluetooth_repository.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../models/ble_device.dart';

class BluetoothRepository {
// Singleton pattern for single source of truth
static final BluetoothRepository _instance = BluetoothRepository._internal();
factory BluetoothRepository() => _instance;
BluetoothRepository._internal();

// Check Bluetooth availability
Future<bool> isBluetoothAvailable() async {
try {
return await FlutterBluePlus.isAvailable;
} catch (e) {
return false;
}
}

// Bluetooth adapter state stream
Stream<BluetoothAdapterState> get adapterStateStream {
return FlutterBluePlus.adapterState;
}

// Request necessary permissions
Future<bool> requestPermissions() async {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;

if (androidInfo.version.sdkInt >= 31) {
// Android 12+
final bluetoothScan = await Permission.bluetoothScan.request();
final bluetoothConnect = await Permission.bluetoothConnect.request();

return bluetoothScan.isGranted && bluetoothConnect.isGranted;
} else {
// Android 11 and below
final location = await Permission.locationWhenInUse.request();
return location.isGranted;
}
} else if (Platform.isIOS) {
// iOS handles permission automatically via CoreBluetooth
return true;
}

return false;
}

// Turn on Bluetooth (Android only)
Future<void> turnOn() async {
if (Platform.isAndroid) {
try {
await FlutterBluePlus.turnOn();
} catch (e) {
throw Exception('Failed to turn on Bluetooth: $e');
}
}
}

// Start BLE scan
Future<void> startScan({
Duration timeout = const Duration(seconds: 15),
List<String>? serviceUuids,
}) async {
try {
// Stop any existing scan
await FlutterBluePlus.stopScan();

// Start new scan
await FlutterBluePlus.startScan(
timeout: timeout,
withServices: serviceUuids?.map((uuid) => Guid(uuid)).toList(),
androidUsesFineLocation: false,
);
} catch (e) {
throw Exception('Scan failed: $e');
}
}

// Stop scan
Future<void> stopScan() async {
try {
await FlutterBluePlus.stopScan();
} catch (e) {
throw Exception('Failed to stop scan: $e');
}
}

// Scan results stream
Stream<List<BleDevice>> get scanResultsStream {
return FlutterBluePlus.scanResults.map((results) {
return results
.map((result) => BleDevice.fromScanResult(result))
.toList();
});
}

// Is scanning stream
Stream<bool> get isScanningStream {
return FlutterBluePlus.isScanning;
}

// Connect to device
Future<void> connect(BluetoothDevice device) async {
try {
await device.connect(
timeout: const Duration(seconds: 15),
autoConnect: false,
);
} catch (e) {
throw Exception('Connection failed: $e');
}
}

// Disconnect device
Future<void> disconnect(BluetoothDevice device) async {
try {
await device.disconnect();
} catch (e) {
throw Exception('Disconnect failed: $e');
}
}

// Connection state stream
Stream<BluetoothConnectionState> connectionStateStream(
BluetoothDevice device,
) {
return device.connectionState;
}

// Discover services
Future<List<BluetoothService>> discoverServices(
BluetoothDevice device,
) async {
try {
return await device.discoverServices();
} catch (e) {
throw Exception('Service discovery failed: $e');
}
}

// Read characteristic
Future<List<int>> readCharacteristic(
BluetoothCharacteristic characteristic,
) async {
try {
return await characteristic.read();
} catch (e) {
throw Exception('Read failed: $e');
}
}

// Write characteristic
Future<void> writeCharacteristic(
BluetoothCharacteristic characteristic,
List<int> value, {
bool withoutResponse = false,
}) async {
try {
await characteristic.write(
value,
withoutResponse: withoutResponse,
);
} catch (e) {
throw Exception('Write failed: $e');
}
}

// Subscribe to notifications
Stream<List<int>> subscribeToCharacteristic(
BluetoothCharacteristic characteristic,
) async* {
try {
await characteristic.setNotifyValue(true);
yield* characteristic.lastValueStream;
} catch (e) {
throw Exception('Subscribe failed: $e');
}
}
}

3. Create BLoC Events

// features/bluetooth/presentation/bloc/bluetooth_event.dart
import 'package:equatable/equatable.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

abstract class BluetoothEvent extends Equatable {
const BluetoothEvent();

@override
List<Object?> get props => [];
}

class CheckBluetoothStatus extends BluetoothEvent {}

class RequestBluetoothPermission extends BluetoothEvent {}

class StartBluetoothScan extends BluetoothEvent {
final Duration timeout;
final List<String>? serviceUuids;

const StartBluetoothScan({
this.timeout = const Duration(seconds: 15),
this.serviceUuids,
});

@override
List<Object?> get props => [timeout, serviceUuids];
}

class StopBluetoothScan extends BluetoothEvent {}

class ConnectToDevice extends BluetoothEvent {
final BluetoothDevice device;

const ConnectToDevice(this.device);

@override
List<Object?> get props => [device];
}

class DisconnectFromDevice extends BluetoothEvent {
final BluetoothDevice device;

const DisconnectFromDevice(this.device);

@override
List<Object?> get props => [device];
}

class DeviceConnectionStateChanged extends BluetoothEvent {
final BluetoothConnectionState state;

const DeviceConnectionStateChanged(this.state);

@override
List<Object?> get props => [state];
}

class ScanResultsUpdated extends BluetoothEvent {
final List<BleDevice> devices;

const ScanResultsUpdated(this.devices);

@override
List<Object?> get props => [devices];
}

class TurnOnBluetooth extends BluetoothEvent {}

4. Create BLoC States

// features/bluetooth/presentation/bloc/bluetooth_state.dart
import 'package:equatable/equatable.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../../data/models/ble_device.dart';

abstract class BluetoothState extends Equatable {
const BluetoothState();

@override
List<Object?> get props => [];
}

class BluetoothInitial extends BluetoothState {}

class BluetoothLoading extends BluetoothState {
final String message;

const BluetoothLoading([this.message = 'Loading...']);

@override
List<Object?> get props => [message];
}

class BluetoothNotAvailable extends BluetoothState {
final String message;

const BluetoothNotAvailable(
[this.message = 'Bluetooth not available on this device'],
);

@override
List<Object?> get props => [message];
}

class BluetoothOff extends BluetoothState {
final String message;

const BluetoothOff(
[this.message = 'Please turn on Bluetooth'],
);

@override
List<Object?> get props => [message];
}

class BluetoothPermissionDenied extends BluetoothState {
final String message;

const BluetoothPermissionDenied(
[this.message = 'Bluetooth permission required'],
);

@override
List<Object?> get props => [message];
}

class BluetoothReady extends BluetoothState {}

class BluetoothScanning extends BluetoothState {
final List<BleDevice> devices;

const BluetoothScanning(this.devices);

@override
List<Object?> get props => [devices];
}

class BluetoothScanComplete extends BluetoothState {
final List<BleDevice> devices;

const BluetoothScanComplete(this.devices);

@override
List<Object?> get props => [devices];
}

class BluetoothConnecting extends BluetoothState {
final BleDevice device;

const BluetoothConnecting(this.device);

@override
List<Object?> get props => [device];
}

class BluetoothConnected extends BluetoothState {
final BleDevice device;
final List<BluetoothService>? services;

const BluetoothConnected(this.device, {this.services});

@override
List<Object?> get props => [device, services];
}

class BluetoothDisconnected extends BluetoothState {
final String? deviceId;

const BluetoothDisconnected([this.deviceId]);

@override
List<Object?> get props => [deviceId];
}

class BluetoothError extends BluetoothState {
final String message;

const BluetoothError(this.message);

@override
List<Object?> get props => [message];
}

5. Implement the BLoC

// features/bluetooth/presentation/bloc/bluetooth_bloc.dart
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../../data/repositories/bluetooth_repository.dart';
import '../../data/models/ble_device.dart';
import 'bluetooth_event.dart';
import 'bluetooth_state.dart';

class BluetoothBloc extends Bloc<BluetoothEvent, BluetoothState> {
final BluetoothRepository _repository;

StreamSubscription? _adapterStateSubscription;
StreamSubscription? _scanResultsSubscription;
StreamSubscription? _connectionStateSubscription;

BluetoothDevice? _connectedDevice;

BluetoothBloc({required BluetoothRepository repository})
: _repository = repository,
super(BluetoothInitial()) {
on<CheckBluetoothStatus>(_onCheckBluetoothStatus);
on<RequestBluetoothPermission>(_onRequestBluetoothPermission);
on<TurnOnBluetooth>(_onTurnOnBluetooth);
on<StartBluetoothScan>(_onStartBluetoothScan);
on<StopBluetoothScan>(_onStopBluetoothScan);
on<ScanResultsUpdated>(_onScanResultsUpdated);
on<ConnectToDevice>(_onConnectToDevice);
on<DisconnectFromDevice>(_onDisconnectFromDevice);
on<DeviceConnectionStateChanged>(_onDeviceConnectionStateChanged);

// Listen to adapter state changes
_listenToAdapterState();
}

void _listenToAdapterState() {
_adapterStateSubscription = _repository.adapterStateStream.listen(
(state) {
if (state == BluetoothAdapterState.off) {
add(CheckBluetoothStatus());
}
},
);
}

Future<void> _onCheckBluetoothStatus(
CheckBluetoothStatus event,
Emitter<BluetoothState> emit,
) async {
try {
emit(const BluetoothLoading('Checking Bluetooth status...'));

final isAvailable = await _repository.isBluetoothAvailable();

if (!isAvailable) {
emit(const BluetoothNotAvailable());
return;
}

final adapterState = await FlutterBluePlus.adapterState.first;

if (adapterState == BluetoothAdapterState.on) {
emit(BluetoothReady());
} else {
emit(const BluetoothOff());
}
} catch (e) {
emit(BluetoothError('Failed to check Bluetooth status: $e'));
}
}

Future<void> _onRequestBluetoothPermission(
RequestBluetoothPermission event,
Emitter<BluetoothState> emit,
) async {
try {
emit(const BluetoothLoading('Requesting permissions...'));

final granted = await _repository.requestPermissions();

if (granted) {
add(CheckBluetoothStatus());
} else {
emit(const BluetoothPermissionDenied());
}
} catch (e) {
emit(BluetoothError('Permission request failed: $e'));
}
}

Future<void> _onTurnOnBluetooth(
TurnOnBluetooth event,
Emitter<BluetoothState> emit,
) async {
try {
emit(const BluetoothLoading('Turning on Bluetooth...'));
await _repository.turnOn();
add(CheckBluetoothStatus());
} catch (e) {
emit(BluetoothError('Failed to turn on Bluetooth: $e'));
}
}

Future<void> _onStartBluetoothScan(
StartBluetoothScan event,
Emitter<BluetoothState> emit,
) async {
try {
// Cancel previous subscription
await _scanResultsSubscription?.cancel();

emit(const BluetoothScanning([]));

// Start scan
await _repository.startScan(
timeout: event.timeout,
serviceUuids: event.serviceUuids,
);

// Listen to scan results
_scanResultsSubscription = _repository.scanResultsStream.listen(
(devices) {
add(ScanResultsUpdated(devices));
},
onError: (error) {
add(StopBluetoothScan());
emit(BluetoothError('Scan error: $error'));
},
);

// Auto-stop after timeout
Future.delayed(event.timeout, () {
if (state is BluetoothScanning) {
add(StopBluetoothScan());
}
});
} catch (e) {
emit(BluetoothError('Failed to start scan: $e'));
}
}

Future<void> _onStopBluetoothScan(
StopBluetoothScan event,
Emitter<BluetoothState> emit,
) async {
try {
await _repository.stopScan();
await _scanResultsSubscription?.cancel();

if (state is BluetoothScanning) {
final devices = (state as BluetoothScanning).devices;
emit(BluetoothScanComplete(devices));
}
} catch (e) {
emit(BluetoothError('Failed to stop scan: $e'));
}
}

void _onScanResultsUpdated(
ScanResultsUpdated event,
Emitter<BluetoothState> emit,
) {
if (state is BluetoothScanning) {
emit(BluetoothScanning(event.devices));
}
}

Future<void> _onConnectToDevice(
ConnectToDevice event,
Emitter<BluetoothState> emit,
) async {
try {
// Stop scanning first
await _repository.stopScan();

final device = BleDevice.fromScanResult(
ScanResult(
device: event.device,
advertisementData: AdvertisementData(
advName: event.device.platformName,
txPowerLevel: null,
appearance: null,
connectable: true,
manufacturerData: {},
serviceData: {},
serviceUuids: [],
),
rssi: -100,
timeStamp: DateTime.now(),
),
);

emit(BluetoothConnecting(device));

// Listen to connection state
_connectionStateSubscription?.cancel();
_connectionStateSubscription = _repository
.connectionStateStream(event.device)
.listen(
(state) => add(DeviceConnectionStateChanged(state)),
);

// Connect
await _repository.connect(event.device);

// Discover services
final services = await _repository.discoverServices(event.device);

_connectedDevice = event.device;
emit(BluetoothConnected(device, services: services));
} catch (e) {
emit(BluetoothError('Connection failed: $e'));
}
}

Future<void> _onDisconnectFromDevice(
DisconnectFromDevice event,
Emitter<BluetoothState> emit,
) async {
try {
await _repository.disconnect(event.device);
await _connectionStateSubscription?.cancel();
_connectedDevice = null;
emit(BluetoothDisconnected(event.device.remoteId.str));
} catch (e) {
emit(BluetoothError('Disconnect failed: $e'));
}
}

void _onDeviceConnectionStateChanged(
DeviceConnectionStateChanged event,
Emitter<BluetoothState> emit,
) {
if (event.state == BluetoothConnectionState.disconnected) {
if (_connectedDevice != null) {
emit(BluetoothDisconnected(_connectedDevice!.remoteId.str));
_connectedDevice = null;
}
}
}

@override
Future<void> close() {
_adapterStateSubscription?.cancel();
_scanResultsSubscription?.cancel();
_connectionStateSubscription?.cancel();
return super.close();
}
}

6. Build the Scan Screen

// features/bluetooth/presentation/screens/device_scan_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/bluetooth_bloc.dart';
import '../bloc/bluetooth_event.dart';
import '../bloc/bluetooth_state.dart';
import '../widgets/device_tile.dart';
import 'device_detail_screen.dart';

class DeviceScanScreen extends StatelessWidget {
const DeviceScanScreen({super.key});

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => BluetoothBloc(
repository: BluetoothRepository(),
)..add(CheckBluetoothStatus()),
child: const DeviceScanView(),
);
}
}

class DeviceScanView extends StatelessWidget {
const DeviceScanView({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('BLE Devices'),
actions: [
BlocBuilder<BluetoothBloc, BluetoothState>(
builder: (context, state) {
if (state is BluetoothScanning) {
return IconButton(
icon: const Icon(Icons.stop),
onPressed: () {
context.read<BluetoothBloc>().add(StopBluetoothScan());
},
);
}

return IconButton(
icon: const Icon(Icons.search),
onPressed: () {
_startScan(context);

Report Page