device_volume 2.0.0 copy "device_volume: ^2.0.0" to clipboard
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.

example/lib/main.dart

import 'dart:async';

import 'package:device_volume/device_volume.dart';
import 'package:flutter/material.dart';

/// Starts the example application used for manual and integration validation.
void main() => runApp(const DeviceVolumeExampleApp());

/// Material application demonstrating every public 2.0 API.
class DeviceVolumeExampleApp extends StatelessWidget {
  const DeviceVolumeExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'device_volume 2.0',
      theme: ThemeData(colorSchemeSeed: Colors.deepPurple, useMaterial3: true),
      home: const DeviceVolumePage(),
    );
  }
}

/// Interactive page that adapts its controls to native capabilities.
class DeviceVolumePage extends StatefulWidget {
  const DeviceVolumePage({super.key});

  @override
  State<DeviceVolumePage> createState() => _DeviceVolumePageState();
}

class _DeviceVolumePageState extends State<DeviceVolumePage> {
  VolumeCapabilities? _capabilities;
  VolumeChannel _channel = VolumeChannel.media;
  VolumeState? _state;
  StreamSubscription<VolumeState>? _subscription;
  String? _error;
  bool _busy = true;

  @override
  void initState() {
    super.initState();
    unawaited(_load());
  }

  @override
  void dispose() {
    unawaited(_subscription?.cancel());
    super.dispose();
  }

  /// Loads capabilities first so unsupported controls are never presented as
  /// guaranteed features, then reads and observes the selected channel.
  Future<void> _load() async {
    setState(() {
      _busy = true;
      _error = null;
    });
    try {
      final capabilities = await DeviceVolume.getCapabilities();
      final channel = capabilities.channels.contains(_channel)
          ? _channel
          : capabilities.channels.first;
      final state = await DeviceVolume.getVolume(channel: channel);
      await _replaceSubscription(channel);
      if (!mounted) return;
      setState(() {
        _capabilities = capabilities;
        _channel = channel;
        _state = state;
        _busy = false;
      });
    } on Object catch (error) {
      _showError(error);
    }
  }

  /// Replaces the native stream whenever the selected logical channel changes.
  Future<void> _replaceSubscription(VolumeChannel channel) async {
    await _subscription?.cancel();
    _subscription = DeviceVolume.streamVolume(channel: channel).listen((state) {
      if (!mounted) return;
      setState(() {
        _state = state;
        _error = null;
      });
    }, onError: _showError);
  }

  /// Reads a newly selected channel and restarts event filtering.
  Future<void> _selectChannel(VolumeChannel channel) async {
    setState(() {
      _channel = channel;
      _busy = true;
    });
    try {
      final state = await DeviceVolume.getVolume(channel: channel);
      await _replaceSubscription(channel);
      if (!mounted) return;
      setState(() {
        _state = state;
        _busy = false;
        _error = null;
      });
    } on Object catch (error) {
      _showError(error);
    }
  }

  /// Applies a normalized slider value and displays the actual native result.
  Future<void> _setVolume(int value) async {
    await _runAction(
      () =>
          DeviceVolume.setVolume(value, channel: _channel, showSystemUi: true),
    );
  }

  /// Runs one volume operation with consistent loading and error handling.
  Future<void> _runAction(Future<VolumeState> Function() action) async {
    setState(() => _busy = true);
    try {
      final state = await action();
      if (!mounted) return;
      setState(() {
        _state = state;
        _busy = false;
        _error = null;
      });
    } on Object catch (error) {
      _showError(error);
    }
  }

  /// Converts native/plugin failures into a visible diagnostics card.
  void _showError(Object error) {
    if (!mounted) return;
    setState(() {
      _error = error.toString();
      _busy = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    final capabilities = _capabilities;
    final state = _state;
    final canWrite =
        capabilities?.writeSupport != VolumeWriteSupport.unsupported;

    return Scaffold(
      appBar: AppBar(
        title: const Text('device_volume 2.0'),
        actions: [
          IconButton(
            key: const ValueKey('refresh'),
            tooltip: 'Refresh capabilities',
            onPressed: _busy ? null : _load,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          if (_error case final error?)
            Card(
              key: const ValueKey('error_card'),
              color: Theme.of(context).colorScheme.errorContainer,
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Text(error),
              ),
            ),
          if (_busy) const LinearProgressIndicator(key: ValueKey('loading')),
          if (capabilities case final value?) ...[
            _SectionCard(
              title: 'Platform capabilities',
              children: [
                _InfoRow(
                  'Channels',
                  value.channels.map((e) => e.name).join(', '),
                ),
                _InfoRow('Read', '${value.canRead}'),
                _InfoRow('Observe', '${value.canObserve}'),
                _InfoRow('Write', value.writeSupport.name),
                _InfoRow('System UI', '${value.canShowSystemUi}'),
              ],
            ),
            const SizedBox(height: 16),
            DropdownButtonFormField<VolumeChannel>(
              key: const ValueKey('channel_selector'),
              initialValue: _channel,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Volume channel',
              ),
              items: [
                for (final channel in value.channels)
                  DropdownMenuItem(value: channel, child: Text(channel.name)),
              ],
              onChanged: _busy
                  ? null
                  : (channel) {
                      if (channel != null) unawaited(_selectChannel(channel));
                    },
            ),
          ],
          if (state case final value?) ...[
            const SizedBox(height: 16),
            _SectionCard(
              title: 'Current state',
              children: [
                Text(
                  '${value.value}%',
                  key: const ValueKey('volume_value'),
                  style: Theme.of(context).textTheme.displayMedium,
                ),
                _InfoRow('Channel', value.channel.name),
                _InfoRow('Muted', '${value.isMuted}'),
                Slider(
                  key: const ValueKey('volume_slider'),
                  value: value.value.toDouble(),
                  max: 100,
                  divisions: 100,
                  label: '${value.value}',
                  onChanged: canWrite && !_busy
                      ? (next) => unawaited(_setVolume(next.round()))
                      : null,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    FilledButton.tonalIcon(
                      key: const ValueKey('decrement'),
                      onPressed: canWrite && !_busy
                          ? () => _runAction(
                              () => DeviceVolume.decrementVolume(
                                channel: _channel,
                                showSystemUi: true,
                              ),
                            )
                          : null,
                      icon: const Icon(Icons.remove),
                      label: const Text('Down'),
                    ),
                    FilledButton.tonalIcon(
                      key: const ValueKey('increment'),
                      onPressed: canWrite && !_busy
                          ? () => _runAction(
                              () => DeviceVolume.incrementVolume(
                                channel: _channel,
                                showSystemUi: true,
                              ),
                            )
                          : null,
                      icon: const Icon(Icons.add),
                      label: const Text('Up'),
                    ),
                  ],
                ),
              ],
            ),
            if (capabilities?.writeSupport == VolumeWriteSupport.bestEffort)
              const Padding(
                padding: EdgeInsets.only(top: 12),
                child: Text(
                  'This platform treats system-volume writes as best effort. '
                  'Always use the returned state as the actual result.',
                  key: ValueKey('best_effort_notice'),
                ),
              ),
          ],
        ],
      ),
    );
  }
}

/// Shared visual container used to keep the example compact and readable.
class _SectionCard extends StatelessWidget {
  const _SectionCard({required this.title, required this.children});

  final String title;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 12),
            ...children,
          ],
        ),
      ),
    );
  }
}

/// Label/value row used by state and capability cards.
class _InfoRow extends StatelessWidget {
  const _InfoRow(this.label, this.value);

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          Expanded(child: Text(label)),
          Text(value, style: const TextStyle(fontWeight: FontWeight.w600)),
        ],
      ),
    );
  }
}
1
likes
160
points
135
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Control and observe the device volume from Flutter through native platform channels on Android, iOS, macOS, Windows, and Linux.

Repository (GitHub)
View/report issues

Topics

#volume #audio #method-channel #platform

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on device_volume

Packages that implement device_volume