device_volume 2.0.0
device_volume: ^2.0.0 copied to clipboard
Control and observe the device volume from Flutter through native platform channels on Android, iOS, macOS, Windows, and Linux.
device_volume #
Control and observe the system output volume from Flutter on Android, iOS, macOS, Windows, and Linux.
Version 2.0 uses Flutter MethodChannel and EventChannel implementations on
every platform. Calls are asynchronous, volume events come from native
observers, and no Dart polling, FFI, JNIgen, or background isolate is required.
Features #
- Read the current volume using one normalized 0–100 scale.
- Set, increment, and decrement volume where the operating system permits it.
- Observe native volume changes, including hardware button changes.
- Address Android media, ring, alarm, notification, voice-call, and system streams.
- Discover runtime capabilities before presenting controls.
- Receive the actual state applied by the platform after every operation.
- Handle stable, typed exceptions with structured native diagnostics.
- Use CocoaPods or Swift Package Manager on Apple platforms.
Requirements #
- Dart
3.11.3or newer within the Dart 3 release line. - Flutter
3.3.0or newer. - Android API 24 or newer.
- iOS 13 or newer.
- macOS 10.15 or newer.
- A Windows host with Core Audio.
- PulseAudio or PipeWire-Pulse on Linux.
The package is developed and validated with Flutter 3.44.6 and Dart 3.12.2. Swift Package Manager integration requires Flutter 3.44 or newer; older Flutter applications continue to use CocoaPods.
Installation #
Add the dependency to your application:
dependencies:
device_volume: ^2.0.0
Then resolve packages:
flutter pub get
No Dart-side initialization or platform permissions are required for normal media-volume access.
Quick start #
import 'package:device_volume/device_volume.dart';
Future<void> configureVolume() async {
// Capabilities can vary by platform and by the selected output device.
final capabilities = await DeviceVolume.getCapabilities();
if (!capabilities.canRead) return;
final current = await DeviceVolume.getVolume();
print('Current volume: ${current.value}%');
if (capabilities.writeSupport != VolumeWriteSupport.unsupported) {
// Always use the returned state: native step sizes can change the result.
final applied = await DeviceVolume.setVolume(50);
print('Applied volume: ${applied.value}%');
}
}
Observe native changes:
final subscription = DeviceVolume.streamVolume().listen(
(state) {
print('${state.channel.name}: ${state.value}%');
},
onError: (Object error) {
print('Volume observation failed: $error');
},
);
// Cancel when the owning widget/service is disposed.
await subscription.cancel();
Public API #
All calls use VolumeChannel.media by default.
Future<VolumeState> DeviceVolume.getVolume({
VolumeChannel channel = VolumeChannel.media,
});
Future<VolumeState> DeviceVolume.setVolume(
int value, {
VolumeChannel channel = VolumeChannel.media,
bool showSystemUi = false,
});
Future<VolumeState> DeviceVolume.incrementVolume({
VolumeChannel channel = VolumeChannel.media,
bool showSystemUi = false,
});
Future<VolumeState> DeviceVolume.decrementVolume({
VolumeChannel channel = VolumeChannel.media,
bool showSystemUi = false,
});
Stream<VolumeState> DeviceVolume.streamVolume({
VolumeChannel channel = VolumeChannel.media,
});
Future<VolumeCapabilities> DeviceVolume.getCapabilities();
VolumeState #
VolumeState is an immutable snapshot:
| Property | Meaning |
|---|---|
value |
Normalized integer from 0 to 100. |
min |
Public minimum, always 0. |
max |
Public maximum, always 100. |
normalized |
Same value represented from 0.0 to 1.0. |
isMuted |
Whether the native output reports a muted/zero state. |
channel |
Logical channel represented by this snapshot. |
Native values are normalized in one Dart boundary. For example, Android media volume 7 in a native 0–15 range is exposed as 47.
VolumeCapabilities #
Capabilities describe the current device rather than only its operating system:
final capabilities = await DeviceVolume.getCapabilities();
if (capabilities.supportsChannel(VolumeChannel.ring)) {
final ring = await DeviceVolume.getVolume(channel: VolumeChannel.ring);
}
writeSupport has three possible values:
supported: the platform exposes a supported native write API;bestEffort: a change can be attempted but is not guaranteed;unsupported: the selected platform/device cannot be changed.
Platform support #
| Platform | Channels | Read | Write | Native events | System UI |
|---|---|---|---|---|---|
| Android | media, ring, alarm, notification, voiceCall, system | Yes | Yes, unless fixed/policy restricted | ContentObserver + deduplication |
Yes |
| iOS device | media | Yes | Best effort through MPVolumeView |
AVAudioSession.outputVolume KVO |
MPVolumeView |
| iOS Simulator | media | Yes | No | KVO | No effective volume UI |
| macOS | media | Yes when the route exposes software volume | Device dependent | CoreAudio property listeners | No |
| Windows | media | Yes | Yes | IAudioEndpointVolumeCallback |
No |
| Linux | media | Yes | Yes | PulseAudio subscriptions | No |
The capabilities returned at runtime are authoritative. Hardware, output routes, device policy, and operating-system versions can reduce support.
Platform details and restrictions #
Android #
The Android plugin uses the application Context supplied by
FlutterPluginBinding and controls public AudioManager stream APIs.
showSystemUi: truemaps toAudioManager.FLAG_SHOW_UI.- Increment/decrement use Android's native stream step.
- Devices reporting
AudioManager.isVolumeFixedexpose write support asunsupported. - Do Not Disturb can block ring or notification changes. Android may throw a
SecurityException, which becomesPermissionDeniedException. - The plugin does not open notification-policy settings or request policy access automatically. The host application owns that user flow.
- Android does not publish a stable public per-stream event. The plugin listens to system settings changes, re-queries supported streams, and deduplicates events without using a Dart timer.
iOS #
Apple exposes AVAudioSession.outputVolume for reading and observing the
system output volume, but does not expose a public programmatic setter.
On a physical device, this package performs a best-effort change using the
slider supplied by MPVolumeView. The returned VolumeState is read after the
request and is the only value applications should treat as applied. The
operating system can ignore the request.
The iOS Simulator cannot change system volume. It reports
VolumeWriteSupport.unsupported, and write attempts throw
UnsupportedOperationException.
Applications intended for App Store distribution should evaluate this best-effort behavior against their own product and review requirements.
macOS #
macOS uses the default CoreAudio output device. The implementation prefers virtual main volume and falls back to scalar master/channel properties.
Some HDMI, DisplayPort, aggregate, and professional audio devices do not expose
software volume. Those devices report write support as unsupported or return
BackendNotAvailableException; the plugin never reports a successful write
without CoreAudio accepting it.
Changing the default output device refreshes listeners and emits the new state.
Windows #
Windows uses IMMDeviceEnumerator and IAudioEndpointVolume for the default
multimedia render endpoint. Volume and mute callbacks are marshalled back to
the Flutter window thread before sending Dart events. Default endpoint changes
release the previous COM interfaces and attach to the new endpoint.
Linux #
Linux uses one reusable PulseAudio context integrated with Flutter's GLib main
loop. It works with PulseAudio and PipeWire's pipewire-pulse compatibility
service.
If no audio server or default sink is available, calls fail with
BackendNotAvailableException. Writes are asynchronous and never run a
blocking PulseAudio main loop on Flutter's platform thread.
Linux build dependencies normally include:
sudo apt install libpulse-dev libpulse-mainloop-glib0
Distribution package names can differ.
Error handling #
All package failures extend DeviceVolumeException:
try {
await DeviceVolume.setVolume(
80,
channel: VolumeChannel.ring,
showSystemUi: true,
);
} on PermissionDeniedException catch (error) {
// Android notification policy/DND can reach this branch.
print(error.details);
} on UnsupportedOperationException catch (error) {
// The platform or current device cannot perform this operation.
print(error.message);
} on DeviceVolumeException catch (error) {
// Stable code plus structured platform diagnostics.
print('${error.code}: ${error.message}');
}
| Exception | Stable code |
|---|---|
UnsupportedOperationException |
unsupported_operation |
InvalidVolumeValueException |
invalid_volume_value |
PermissionDeniedException |
permission_denied |
BackendNotAvailableException |
backend_not_available |
NativeBackendException |
native_backend_failure |
VolumeObservationException |
volume_observation_failure |
PluginDetachedException |
plugin_detached |
details can contain platform, operation, backend, channel, and native
diagnostic information.
Architecture #
The Dart layer uses two fixed channel names:
dev.arcas.device_volume/methodsfor commands;dev.arcas.device_volume/eventsfor native changes.
One native plugin instance is created per Flutter Engine. Native observers and callbacks start when Dart listens, stop when the last subscription is cancelled, and are also released when the engine detaches.
The Dart event transport owns one broadcast EventChannel subscription and
filters states by VolumeChannel. This avoids one native observer per Dart
listener.
Migrating from 0.1.x #
Version 2.0 intentionally changes the public API from synchronous integers to
asynchronous VolumeState objects.
Read volume:
// 0.1.x
final value = DeviceVolume.getVolume();
// 2.0.0
final state = await DeviceVolume.getVolume();
final value = state.value;
Set volume:
// 0.1.x
final value = DeviceVolume.setVolume(50);
// 2.0.0
final state = await DeviceVolume.setVolume(50);
final value = state.value;
Observe volume:
// 0.1.x
DeviceVolume.streamVolume().listen((int value) {});
// 2.0.0
DeviceVolume.streamVolume().listen((VolumeState state) {});
All getVolumeCompute, setVolumeCompute, incrementVolumeCompute, and
decrementVolumeCompute methods were removed. Platform channels are already
asynchronous and do not require compute().
Testing and contributing #
Run Dart analysis and unit tests:
flutter analyze
flutter test
Run the hardware-aware integration suite from the example application:
cd example
flutter test integration_test -d <device-id>
Integration tests restore the original volume after supported write checks. iOS write behavior must be validated on a physical device; simulator coverage only verifies reads, events, capabilities, and expected write rejection.
Native build checks:
cd example
flutter build apk --debug
flutter build ios --simulator --debug --no-codesign
flutter build macos --debug
flutter build windows --debug
flutter build linux --debug
The repository includes official Dart and Flutter agent skills in
.agents/skills. Development validation uses the official Dart/Flutter MCP
server for formatting, analysis, tests, runtime errors, devices, and
integration flows when an MCP-capable client is available.
License #
MIT. See LICENSE.