navideck/universal_ble/universal-ble-migrate-from-flutter-blue-plus
Claude Code and agent skill by navideck/universal_ble
- community
Checking that the SKILL.md file is still in the repository.
Migrate a Flutter app from flutter_blue_plus (FlutterBluePlus) to universal_ble. Use when the user wants to replace flutter_blue_plus, port BLE scanning, connecting, service discovery, read/write or subscription code, or map FlutterBluePlus APIs (BluetoothDevice, Guid, ScanResult, BluetoothCharacteristic, BluetoothDescriptor) to universal_ble equivalents.
Install
npx vskill@latest install navideck/universal_ble/universal-ble-migrate-from-flutter-blue-plusSKILL.md
Migrating from flutter_blue_plus to universal_ble
This skill guides a mechanical, central-role migration from flutter_blue_plus (FlutterBluePlus, "FBP") to universal_ble (UniversalBle, "UB"). Both are Flutter BLE central-role plugins; most concepts map 1:1, but the object model and a handful of APIs differ.
Migration workflow
Work top to bottom, keeping the project compiling after each step:
- Swap the dependency and imports.
- Port adapter/availability state handling.
- Port scanning.
- Port connecting and service discovery.
- Port read/write and descriptor access.
- Port notifications/indications.
- Replace FBP error handling with UB's typed exceptions.
- Update platform setup (Windows especially).
- Run
flutter analyzeand fix remaining references toflutter_blue_plus.
Search the codebase for FlutterBluePlus, BluetoothDevice, BluetoothCharacteristic, BluetoothDescriptor, ScanResult, Guid, and import 'package:flutter_blue_plus.
1. Dependency and imports
Remove flutter_blue_plus (and flutter_blue_plus_winrt if present) and add universal_ble:
Replace imports:
Windows previously required the separate flutter_blue_plus_winrt package; UB supports Android, iOS, macOS, Windows, Linux, and Web from the single package.
2. Mental model
| Concept | flutter_blue_plus | universal_ble | |
|---|---|---|---|
| Entry point | FlutterBluePlus (static) | UniversalBle (static) | |
| Device handle | BluetoothDevice object | BleDevice (high level) or a String deviceId (low level) | |
| Identifiers | Guid objects (.str) | Plain String UUIDs, any format/case | |
| Results | ScanResult wrappers | BleDevice directly | |
| Device operations | Methods on BluetoothDevice | Extension methods on BleDevice and/or UniversalBle statics |
Key differences to internalize:
- UUIDs are plain strings. No
Guidwrapper. Pass"180D","180d", or the
full 128-bit form interchangeably; UB normalizes everything to lowercase 128-bit (e.g. 0000180d-0000-1000-8000-00805f9b34fb). Use BleUuidParser.string(), BleUuidParser.number(), or BleUuidParser.compare() if you need explicit conversion.
- Two styles. Prefer the high-level
BleDevice/ extension API (closest to FBP).
The low-level UniversalBle.read(deviceId, service, characteristic, ...) statics are for deviceId-only workflows. Do not mix object methods with a device you never obtained as a BleDevice.
- Streams never close and never emit errors. Cancel subscriptions yourself.
3. Symbol mapping
Adapter / availability
| flutter_blue_plus | universal_ble | ||||
|---|---|---|---|---|---|
FlutterBluePlus.adapterState (Stream) | UniversalBle.availabilityStream (Stream) | ||||
FlutterBluePlus.adapterStateNow | await UniversalBle.getBluetoothAvailabilityState() | ||||
BluetoothAdapterState.on | AvailabilityState.poweredOn | ||||
BluetoothAdapterState.off | AvailabilityState.poweredOff | ||||
BluetoothAdapterState.unauthorized | AvailabilityState.unauthorized | ||||
BluetoothAdapterState.unavailable | AvailabilityState.unsupported | ||||
BluetoothAdapterState.unknown | AvailabilityState.unknown | ||||
FlutterBluePlus.turnOn() / turnOff() | UniversalBle.enableBluetooth() / disableBluetooth() (Android/Windows/Linux; returns bool) | ||||
FlutterBluePlus.isSupported | No direct API. Treat AvailabilityState.unsupported as unsupported | ||||
FlutterBluePlus.setLogLevel(LogLevel.x) | UniversalBle.setLogLevel(BleLogLevel.x) | ||||
FlutterBluePlus.setOperationQueueMode(...) | `UniversalBle.queueType = QueueType.auto | global | perDevice | none` | |
FlutterBluePlus.setOptions(restoreState: true) | No direct API. iOS state restoration follows the bluetooth-central background mode automatically | ||||
FlutterBluePlus.setLogLevel logs stream | No equivalent; use the platform logger |
FBP's initial adapter state is often unknown on iOS; UB reports unknown too, so keep any "wait for poweredOn" logic.
Scanning
| flutter_blue_plus | universal_ble | |
|---|---|---|
FlutterBluePlus.startScan(withServices: [Guid], withNames:, withRemoteIds:, timeout:, androidUsesFineLocation:) | UniversalBle.startScan(scanFilter: ScanFilter(withServices: [...], withNamePrefix: [...]), platformConfig: ...) | |
FlutterBluePlus.stopScan() | UniversalBle.stopScan() | |
FlutterBluePlus.onScanResults / scanResults (Stream<List<ScanResult>>) | UniversalBle.scanStream (Stream<BleDevice>, one device per event) | |
FlutterBluePlus.lastScanResults | No equivalent; keep your own cache | |
FlutterBluePlus.isScanning / isScanningNow | UniversalBle.isScanning() | |
FlutterBluePlus.cancelWhenScanComplete(sub) | Manually sub.cancel() | |
ScanResult.device | the BleDevice event itself | |
ScanResult.rssi | bleDevice.rssi | |
ScanResult.advertisementData.advName | bleDevice.name | |
ScanResult.advertisementData.serviceUuids | bleDevice.services | |
ScanResult.advertisementData.manufacturerData | bleDevice.manufacturerDataList (ManufacturerData) | |
ScanResult.advertisementData.serviceData | bleDevice.serviceData | |
Guid("180D") in filters | "180D" |
Notable differences:
- No `timeout:` parameter. Implement the FBP timeout yourself:
``dart UniversalBle.startScan(scanFilter: ScanFilter(withServices: ['180D'])); Timer(const Duration(seconds: 15), () => UniversalBle.stopScan()); ``
- No list batching.
scanStreamemits oneBleDeviceper result, not a list.
There is no "clears between scans" distinction; maintain your own list if needed.
- No `withRemoteIds`. Filter scan results in Dart, or use
ScanFilter.exclusionFilters. - Android fine location: pass
platformConfig: PlatformConfig(android: AndroidOptions(requestLocationPermission: true))instead ofandroidUsesFineLocation:.
Devices
| flutter_blue_plus | universal_ble | |
|---|---|---|
BluetoothDevice.fromId(id) | BleDevice(deviceId: id, name: null) or call UniversalBle.* statics with id | |
device.remoteId.str | bleDevice.deviceId | |
device.platformName | bleDevice.name (sanitized) or bleDevice.rawName (raw) | |
device.advName | bleDevice.name | |
device.connect(mtu:, autoConnect:) | bleDevice.connect(autoConnect: ...); call requestMtu() separately | |
device.disconnect() | bleDevice.disconnect() | |
device.isConnected (sync) | await bleDevice.isConnected (async) | |
device.isDisconnected | !await bleDevice.isConnected | |
device.connectionState (Stream enum) | bleDevice.connectionStream (Stream<bool>) | |
device.connectionState snapshot | await bleDevice.connectionState returns BleConnectionState | |
device.disconnectReason | BleConnectionChange.errorCode / .error from bleDevice.connectionChangeStream | |
device.discoverServices() | await bleDevice.discoverServices() | |
device.servicesList | No equivalent; the list returned by discoverServices() is cached — use getService()/getCharacteristic() | |
device.onServicesReset | No equivalent | |
device.mtu / mtuNow | No stream; await bleDevice.requestMtu(n) returns the negotiated value | |
device.readRssi() | await bleDevice.readRssi() | |
device.requestMtu(n) | await bleDevice.requestMtu(n) | |
device.requestConnectionPriority(ConnectionPriority.high) | UniversalBle.requestConnectionPriority(deviceId, BleConnectionPriority.highPerformance) | |
device.createBond() / removeBond() / bondState | bleDevice.pair() / unpair() / pairingStateStream | |
device.clearGattCache() | discoverServices(platformConfig: DiscoverServicesPlatformConfig(android: AndroidDiscoverServicesOptions(clearGattCache: true))) | |
device.cancelWhenDisconnected(sub) | Manually sub.cancel() when the connection stream goes false | |
FlutterBluePlus.connectedDevices | No equivalent; track the devices your app connected to | |
FlutterBluePlus.systemDevices(withServices) | UniversalBle.getSystemDevices(withServices: [...]) |
getSystemDevices on Apple requires withServices; without it UB defaults to generic 18XX services. You must still connect() to system devices before use.
Characteristics and descriptors
| flutter_blue_plus | universal_ble | |
|---|---|---|
characteristic.uuid.str | characteristic.uuid | |
characteristic.properties.read | characteristic.properties.contains(CharacteristicProperty.read) | |
characteristic.read() | await characteristic.read() | |
characteristic.write(v) | await characteristic.write(v) (with response by default) | |
characteristic.write(v, withoutResponse: true) | await characteristic.write(v, withResponse: false) | |
characteristic.setNotifyValue(true) | await characteristic.notifications.subscribe() | |
characteristic.setNotifyValue(false) | await characteristic.unsubscribe() | |
characteristic.isNotifying | characteristic.isSubscribed | |
characteristic.onValueReceived (Stream<List<int>>) | characteristic.onValueReceived (Stream<Uint8List>) | |
characteristic.lastValue / lastValueStream | No equivalent; use onValueReceived (it fires on read() too) | |
characteristic.descriptors | characteristic.descriptors | |
descriptor.read() / descriptor.write() | characteristic.descriptor(uuid).read() / .write(Uint8List) |
Boolean inversion: FBP useswithoutResponse:; UB useswithResponse:.FBP'swrite(data)(no flag) means with response; UB'swrite(data)alsodefaults towithResponse: true, so a direct call ports unchanged.
For indications, use characteristic.indications.subscribe() instead of notifications.subscribe().
Events API
FBP's global FlutterBluePlus.events.* streams map to UB streams/handlers:
| flutter_blue_plus | universal_ble | |
|---|---|---|
events.onConnectionStateChanged | UniversalBle.onConnectionChange = (BleConnectionChange change) {} or UniversalBle.connectionChangeStream(deviceId) | |
events.onMtuChanged | No equivalent stream | |
events.onReadRssi | UniversalBle.readRssi(deviceId) | |
events.onServicesReset | No equivalent | |
events.onDiscoveredServices | No equivalent | |
events.onCharacteristicReceived | UniversalBle.onValueChange = (BleCharacteristicValue value) {} | |
events.onCharacteristicWritten | No equivalent | |
events.onDescriptorRead / onDescriptorWritten | No equivalent | |
events.onBondStateChanged | UniversalBle.onPairingStateChange = (deviceId, isPaired) {} | |
events.onNameChanged | No equivalent |
Prefer the per-device/stream form (scanStream, connectionStream, characteristicValueStream, stream.listen) over the global onX handlers when you already hold the relevant object.
4. Before / after examples
Adapter state
Filtered scan
Connect and discover services
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/universal-ble-migrate-from-flutter-blue-plus/SKILL.md
Popularity
- Repository stars
- 171
- Repository forks
- 77
- 7-day trend score
- 0
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/navideck/universal_ble/universal-ble-migrate-from-flutter-blue-plus)