Flutter: Monitor Bluetooth Connection State & Display Popup
Hey guys! Ever wanted to build a Flutter app that reacts instantly when your Bluetooth device disconnects? Maybe you want to show a friendly popup, update the UI, or trigger some other action. In this article, we'll dive deep into how to achieve this using flutter_blue_plus and some clever state management techniques.
Setting Up Flutter Blue Plus
First things first, let's get flutter_blue_plus set up. This package is a powerful tool for interacting with Bluetooth devices in your Flutter app. To add it to your project, simply include it in your pubspec.yaml file:
dependencies:
flutter_blue_plus: ^latest_version
Replace ^latest_version with the latest version number from pub.dev. Once you've added the dependency, run flutter pub get to fetch the package.
Now, let’s initialize FlutterBluePlus. A good place to do this is in your main.dart file, before your app starts:
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterBluePlus.initialize();
runApp(MyApp());
}
This ensures that FlutterBluePlus is ready to go when your app starts up. Initialization involves setting up the underlying Bluetooth stack and checking for necessary permissions, making it a crucial first step.
Listening to Connection State Changes
The heart of our solution lies in listening to the connection state of your Bluetooth device. flutter_blue_plus provides a stream of connection states that we can tap into. Here’s how:
First, you need to obtain a BluetoothDevice object representing the device you're interested in. You can get this device object after scanning and discovering the device. Once you have the BluetoothDevice instance, you can listen to its connection state like this:
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
// Assuming you have a BluetoothDevice object named 'device'
device.connectionState.listen((BluetoothConnectionState state) {
if (state == BluetoothConnectionState.disconnected) {
// Device disconnected! Show your popup here.
showDisconnectedPopup();
}
});
This code snippet sets up a listener that fires whenever the connection state of the device changes. When the state is BluetoothConnectionState.disconnected, it means the device has lost connection, and we can trigger our popup.
Implementing the Popup
Now, let's create the showDisconnectedPopup function. This function will display a popup (typically an AlertDialog or a custom widget) to inform the user about the disconnection. Here’s a basic example:
import 'package:flutter/material.dart';
void showDisconnectedPopup(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Device Disconnected'),
content: Text('The Bluetooth device has been disconnected.'),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
Don't forget to pass the BuildContext to the showDisconnectedPopup function. You can achieve this by storing the BuildContext in a variable accessible from your connection state listener or by using a GlobalKey<NavigatorState>. The latter is particularly useful if you need to show the popup from anywhere in your app.
Integrating with State Management
To display the popup from anywhere in your app, you'll likely need to integrate this functionality with your state management solution. Here are a few popular approaches:
1. Provider
If you're using Provider, you can create a BluetoothProvider that manages the connection state. This provider can listen to the connection state changes and notify listeners when the device disconnects.
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:provider/provider.dart';
class BluetoothProvider with ChangeNotifier {
bool _isConnected = false;
bool get isConnected => _isConnected;
set isConnected(bool value) {
_isConnected = value;
notifyListeners();
}
void startListening(BluetoothDevice device, BuildContext context) {
device.connectionState.listen((BluetoothConnectionState state) {
if (state == BluetoothConnectionState.connected) {
isConnected = true;
} else {
isConnected = false;
showDisconnectedPopup(context);
}
});
}
}
Then, in your UI, you can use Consumer to listen to changes in the BluetoothProvider and display the popup accordingly.
2. Riverpod
Riverpod is another excellent state management solution. You can create a StateProvider or StateNotifierProvider to manage the connection state and trigger the popup.
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final bluetoothProvider = StateProvider<bool>((ref) => false);
void startListening(WidgetRef ref, BluetoothDevice device, BuildContext context) {
device.connectionState.listen((BluetoothConnectionState state) {
if (state == BluetoothConnectionState.connected) {
ref.read(bluetoothProvider.notifier).state = true;
} else {
ref.read(bluetoothProvider.notifier).state = false;
showDisconnectedPopup(context);
}
});
}
In your UI, you can use Consumer or ConsumerWidget to access the bluetoothProvider and react to connection state changes.
3. BLoC/Cubit
If you're using BLoC or Cubit, you can create a BluetoothCubit that emits states based on the connection status. Your UI can then listen to these states and display the popup when a disconnection state is emitted.
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Define the states
sealed class BluetoothState {}
class BluetoothConnected extends BluetoothState {}
class BluetoothDisconnected extends BluetoothState {}
class BluetoothCubit extends Cubit<BluetoothState> {
BluetoothCubit() : super(BluetoothConnected()); // Assuming initial state is connected
void startListening(BluetoothDevice device, BuildContext context) {
device.connectionState.listen((BluetoothConnectionState state) {
if (state == BluetoothConnectionState.connected) {
emit(BluetoothConnected());
} else {
emit(BluetoothDisconnected());
showDisconnectedPopup(context);
}
});
}
}
Your UI will use a BlocListener or BlocBuilder to listen for state changes and trigger the popup when the BluetoothDisconnected state is emitted.
Handling Context Issues
A common challenge is accessing the BuildContext from within the connection state listener. Since the listener might be triggered outside of a widget's build method, you might not have a readily available BuildContext. Here are a couple of solutions:
1. GlobalKey
Create a global GlobalKey<NavigatorState> and use it to access the BuildContext of your app's navigator.
import 'package:flutter/material.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
// ... other configurations
);
}
}
void showDisconnectedPopup() {
Navigator.of(navigatorKey.currentContext!).push(
MaterialPageRoute(builder: (context) => DisconnectedPopup()),
);
}
class DisconnectedPopup extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Device Disconnected'),
content: Text('The Bluetooth device has been disconnected.'),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
}
2. Passing Context from Widget
Pass the BuildContext from a widget that has access to it to your Bluetooth management class or function. This can be done through a constructor or a method parameter.
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
BluetoothManager.instance.startListening(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bluetooth App')),
body: Center(child: Text('Listening for Bluetooth disconnections')),
);
}
}
class BluetoothManager {
static final BluetoothManager instance = BluetoothManager._internal();
factory BluetoothManager() => instance;
BluetoothManager._internal();
void startListening(BuildContext context) {
// Start listening to Bluetooth connection state changes
// Use the context to show the popup
}
}
Best Practices and Considerations
- Error Handling: Always wrap your Bluetooth operations in
try-catchblocks to handle potential errors gracefully. - Permissions: Ensure your app requests the necessary Bluetooth permissions from the user.
- Background Operations: Be mindful of background Bluetooth operations, especially on iOS. Use appropriate background modes if needed.
- User Experience: Design your popup to be informative and user-friendly. Provide options for the user to reconnect or troubleshoot the issue.
Complete Example
Here’s a complete example that combines the above concepts:
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterBluePlus.initialize();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
title: 'Bluetooth Disconnection Alert',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
BluetoothDevice? _connectedDevice;
@override
void initState() {
super.initState();
_startScanAndConnect();
}
Future<void> _startScanAndConnect() async {
FlutterBluePlus.scanResults.listen((List<ScanResult> results) {
for (ScanResult result in results) {
if (result.device.name == 'YourDeviceName') { // Replace with your device name
_connectToDevice(result.device);
break;
}
}
});
FlutterBluePlus.startScan(timeout: Duration(seconds: 4));
}
Future<void> _connectToDevice(BluetoothDevice device) async {
try {
await device.connect();
_connectedDevice = device;
_listenToConnectionState(device);
} catch (e) {
print('Error connecting to device: $e');
}
}
void _listenToConnectionState(BluetoothDevice device) {
device.connectionState.listen((BluetoothConnectionState state) {
if (state == BluetoothConnectionState.disconnected) {
_showDisconnectedPopup();
}
});
}
void _showDisconnectedPopup() {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: navigatorKey.currentContext!,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Device Disconnected'),
content: Text('The Bluetooth device has been disconnected.'),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bluetooth Disconnection Alert')),
body: Center(
child: Text(_connectedDevice != null
? 'Connected to: ${_connectedDevice!.name}'
: 'Scanning for devices...'),
),
);
}
}
Conclusion
And there you have it! You've learned how to listen to Bluetooth connection state changes in Flutter using flutter_blue_plus and display a popup when a device disconnects. By integrating this functionality with your state management solution and handling context issues, you can create a seamless and informative user experience.
Remember to adapt the code to your specific needs and always prioritize error handling and user experience. Happy coding, and may your Bluetooth connections always be stable!