Flutter wrapper around Android's CompanionDeviceManager (CDM).
Show the system-rendered accessory picker, let the user choose a nearby
Bluetooth Low Energy peripheral filtered by Service UUID and/or name pattern,
and receive the associated device back in Dart — without writing your own
scan UI or asking the user for BLUETOOTH_SCAN/ACCESS_FINE_LOCATION runtime
permissions.
CDM is available from Android 8.0 (API 26).
This plugin is the Android counterpart to
flutter_accessorysetup (which wraps iOS AccessorySetupKit). Use
both side-by-side for cross-platform OS-managed pairing.
dependencies:
flutter_android_cdm: ^0.0.2import 'package:flutter_android_cdm/flutter_android_cdm.dart';
final cdm = FlutterAndroidCdm.instance;
try {
final device = await cdm.associate(const CdmRequest(
serviceUuid: '0000abcd-0000-1000-8000-00805f9b34fb', // your 128-bit UUID
namePattern: r'^MyDevice-',
singleDevice: false,
));
print('Paired: ${device.name} @ ${device.address}');
} on CdmException catch (e) {
if (e.code == CdmErrorCode.cancelled) {
// User dismissed the picker.
} else {
rethrow;
}
}CdmRequest exposes the optional parts of AssociationRequest.Builder:
await cdm.associate(const CdmRequest(
namePattern: r'^MyWatch-',
deviceProfile: CdmDeviceProfile.watch, // changes dialog copy + capabilities
displayName: 'My Watch', // name the system shows (API 33+)
forceConfirmation: true, // always show the chooser (API 33+)
));deviceProfilemaps toAssociationRequest.DEVICE_PROFILE_*. A profile is not cosmetic: it changes the system dialog copy, grants that profile's capabilities, and may require the app to hold a specific permission. Profiles are version-gated (watchAPI 31;computer/appStreaming/automotiveProjectionAPI 33;glasses/nearbyDeviceStreamingAPI 34). Requesting one the device can't support throwsCdmErrorCode.unsupported.displayName/forceConfirmationrequire API 33+ and are silently ignored on older devices.
You cannot put a custom icon (or a Flutter asset) into the standard CDM
picker — the dialog is rendered by the system. Its icon, layout, and body copy
are not app-customizable; the body text is a function of deviceProfile only.
A custom icon is possible only in the self-managed confirmation dialog
(setDeviceIcon, API 36.1+), which forfeits CDM's system scanning and is not
exposed by this plugin.
This plugin contributes <uses-feature android:name="android.software.companion_device_setup"/>
via manifest merger, but the host app still needs the Bluetooth permissions
it intends to use after pairing — the picker itself doesn't require runtime
permissions, but reading the device name and connecting do.
android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>android/app/build.gradle.kts:
defaultConfig {
minSdk = 26
}FlutterAndroidCdm.associate(CdmRequest)→Future<AssociatedDevice>FlutterAndroidCdm.getAssociations()→Future<List<AssociatedDevice>>FlutterAndroidCdm.disassociate(AssociatedDevice)→Future<void>CdmRequest({serviceUuid, namePattern, singleDevice, deviceProfile, displayName, forceConfirmation})CdmDeviceProfile—watch,computer,appStreaming,automotiveProjection,glasses,nearbyDeviceStreamingAssociatedDevice(address, name, id)CdmException(code, message)with codes:cancelled,busy,unsupported,cdm_failure,launch_failed,no_device,no_activity,bad_args,engine_detached,activity_detached,disassociate_failed,get_associations_failed
See example/ for a runnable demo.
Pre-fill the picker filters via build args so you don't have to type them on
every run. Copy example/cdm-config.example.json to example/cdm-config.json
(gitignored — keep your own device identifiers out of the repo), edit, then:
flutter run --dart-define-from-file=cdm-config.jsonBoth keys are optional; their absence leaves the corresponding TextField
empty, and the UI still lets the user type values at runtime.
BSD 3-Clause — see LICENSE.