Troubleshooting
Every failure is a typed exception, so you can handle the ones you can recover from and let the rest surface.
Install#
flutter pub add flutter_classic_bluetoothimport 'package:flutter_classic_bluetooth/flutter_classic_bluetooth.dart';The exceptions#
| Exception | Means |
|---|---|
BtcUnsupportedException | The platform has no API for this |
BtcPermissionException | The user denied a permission, or it is missing from the manifest |
BtcDisabledException | The adapter is off |
BtcConnectionException | The connection was refused or lost |
BtcWriteException | A write failed, usually because the link is gone |
BtcDiscoveryException | Discovery could not start |
BtcTimeoutException | The deadline passed first |
BtcAddressException | The MAC address is malformed |
BtcUuidException | The UUID is malformed |
All of them extend BtcException, so a single catch covers everything while you narrow the cases you can act on.
try {
final connection = await bluetooth.connect(
address: device.address,
timeout: const Duration(seconds: 8),
);
} on BtcPermissionException {
showSettingsPrompt();
} on BtcDisabledException {
askUserToEnableBluetooth();
} on BtcTimeoutException {
showRetry('Device did not respond. Is it powered on?');
} on BtcException catch (e) {
report(e.message);
}Nothing happens when I scan#
In order of how often it turns out to be the cause: the manifest is missing the permissions, the user denied the runtime prompt, the adapter is off, or you are on iOS, which has no discovery API at all. Work through it rather than guessing:
final caps = await bluetooth.getPlatformCapabilities();
print('can discover: ${caps.canDiscoverDevices}');
print('supported: ${await bluetooth.isSupported()}');
print('enabled: ${await bluetooth.isEnabled()}');On Android 11 and below, remember that scanning needs location permission, and a device with location services switched off at the system level returns an empty scan with no error.
Connect hangs forever#
Pass a timeout. Without one the native call decides how long to block, and for a device that is off or out of range that can be a very long time. A native connect cannot be cancelled, so an attempt that lands after your deadline is closed and released for you rather than leaking a socket.
Connects, then drops right away#
Usually power on the device side rather than anything in software. Modules driven from a pin that cannot supply the peak current during transmit brown out the moment the radio comes up. If the device is already connected to something else, most modules accept exactly one link and refuse the second.
Data arrives split or merged#
Expected. RFCOMM is a byte stream, and event boundaries mean nothing. Use lines or frames rather than treating each event as a message. This is covered in full on the send and receive page.
Received text is garbage#
On an HC-05 or similar module, a baud mismatch between the sketch and the module. On a thermal printer, a code page mismatch. Both produce a working connection that delivers the wrong bytes, which is why they are easy to mistake for a plugin problem.
The last thing I sent never arrived#
await connection.output.writeBytes(payload);
await connection.output.allSent;
await connection.finish();close drops whatever is still queued. finish flushes first. Use finish unless you are tearing down after an error and no longer care.
It worked, then stopped after reflashing#
The bond went stale. Unpair the device in system settings and pair again. This is common with ESP32 boards, where reflashing can change what the device advertises.
Works on Android, not on iOS#
iOS only reaches MFi certified accessories whose protocol string is declared in Info.plist, and it cannot discover devices at all. A generic HC-05 or ESP32 is unreachable on iOS. This is an Apple restriction, not something a plugin can work around.
Reporting something#
If none of this fits, the issue tracker is the place. Include the platform and version, the device you are talking to, the exception type and message, and the smallest snippet that reproduces it.