Skip to main content

simplest_power_utilities 1.1.0 copy "simplest_power_utilities: ^1.1.0" to clipboard
simplest_power_utilities: ^1.1.0 copied to clipboard

Plugin which exposes various power-related utilities for Android and iOS, including low power mode detection

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:simplest_power_utilities/simplest_power_utilities.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'simplest_power_utilities example',
      theme: ThemeData(colorSchemeSeed: Colors.green),
      home: const PowerUtilitiesPage(),
    );
  }
}

String _formatTime(DateTime time) {
  String pad(int value) => value.toString().padLeft(2, '0');
  return '${pad(time.hour)}:${pad(time.minute)}:${pad(time.second)}';
}

/// Demonstrates every API exposed by [SimplestPowerUtilities].
class PowerUtilitiesPage extends StatefulWidget {
  const PowerUtilitiesPage({super.key});

  @override
  State<PowerUtilitiesPage> createState() => _PowerUtilitiesPageState();
}

/// A single low power mode change received from [SimplestPowerUtilities.onLowPowerModeChanged].
class LowPowerModeChange {
  const LowPowerModeChange({required this.enabled, required this.receivedAt});

  final bool enabled;
  final DateTime receivedAt;
}

class _PowerUtilitiesPageState extends State<PowerUtilitiesPage> {
  final _plugin = const SimplestPowerUtilities();

  bool? _lowPowerModeEnabled;
  String? _statusError;
  bool _loadingStatus = false;

  StreamSubscription<bool>? _subscription;
  String? _streamError;
  final List<LowPowerModeChange> _changes = <LowPowerModeChange>[];

  bool _launchingSettings = false;

  int? _batteryLevel;
  String? _batteryError;
  bool _loadingBatteryLevel = false;
  DateTime? _batteryUpdatedAt;
  StreamSubscription<int>? _batterySubscription;

  ChargingState? _chargingState;
  String? _chargingError;
  bool _loadingChargingState = false;
  DateTime? _chargingUpdatedAt;
  StreamSubscription<ChargingState>? _chargingSubscription;

  @override
  void initState() {
    super.initState();
    _refreshStatus();
    _refreshBatteryLevel();
    _startBatteryUpdates();
    _refreshChargingState();
    _startChargingUpdates();
  }

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

  // Charging state: one-off query.
  Future<void> _refreshChargingState() async {
    setState(() => _loadingChargingState = true);
    ChargingState? state;
    String? error;
    try {
      state = await _plugin.getChargingState();
    } on PlatformException catch (e) {
      error = e.message ?? e.code;
    }
    if (!mounted) {
      return;
    }
    setState(() {
      _loadingChargingState = false;
      _chargingError = error;
      if (state != null) {
        _chargingState = state;
        _chargingUpdatedAt = DateTime.now();
      }
    });
  }

  // Charging state: live stream, emits the current state first and then every
  // change.
  void _startChargingUpdates() {
    setState(() {
      _chargingSubscription = _plugin.onChargingStateChanged.listen(
        (ChargingState state) {
          setState(() {
            _chargingState = state;
            _chargingError = null;
            _chargingUpdatedAt = DateTime.now();
          });
        },
        onError: (Object error) {
          setState(() {
            _chargingError = error is PlatformException
                ? error.message ?? error.code
                : '$error';
          });
        },
      );
    });
  }

  Future<void> _stopChargingUpdates() async {
    final subscription = _chargingSubscription;
    setState(() => _chargingSubscription = null);
    await subscription?.cancel();
  }

  // Battery level: one-off query.
  Future<void> _refreshBatteryLevel() async {
    setState(() => _loadingBatteryLevel = true);
    int? level;
    String? error;
    try {
      level = await _plugin.getBatteryLevel();
    } on PlatformException catch (e) {
      error = e.code == 'UNAVAILABLE'
          ? 'Battery level is not available on this device.'
          : e.message ?? e.code;
    }
    if (!mounted) {
      return;
    }
    setState(() {
      _loadingBatteryLevel = false;
      _batteryError = error;
      if (level != null) {
        _batteryLevel = level;
        _batteryUpdatedAt = DateTime.now();
      }
    });
  }

  // Battery level: live stream, emits the current level first and then every
  // change.
  void _startBatteryUpdates() {
    setState(() {
      _batterySubscription = _plugin.onBatteryLevelChanged.listen(
        (int level) {
          setState(() {
            _batteryLevel = level;
            _batteryError = null;
            _batteryUpdatedAt = DateTime.now();
          });
        },
        onError: (Object error) {
          setState(() {
            _batteryError = error is PlatformException
                ? error.message ?? error.code
                : '$error';
          });
        },
      );
    });
  }

  Future<void> _stopBatteryUpdates() async {
    final subscription = _batterySubscription;
    setState(() => _batterySubscription = null);
    await subscription?.cancel();
  }

  // Basic usage: read the current low power mode status.
  Future<void> _refreshStatus() async {
    setState(() {
      _loadingStatus = true;
      _statusError = null;
    });
    bool? enabled;
    String? error;
    try {
      enabled = await _plugin.isLowPowerModeEnabled();
    } on PlatformException catch (e) {
      error = e.message ?? e.code;
    }
    if (!mounted) {
      return;
    }
    setState(() {
      _lowPowerModeEnabled = enabled;
      _statusError = error;
      _loadingStatus = false;
    });
  }

  // Real-time updates: subscribe to low power mode changes.
  void _startListening() {
    setState(() {
      _streamError = null;
      _subscription = _plugin.onLowPowerModeChanged.listen(
        (bool enabled) {
          setState(() {
            _lowPowerModeEnabled = enabled;
            _changes.insert(
              0,
              LowPowerModeChange(enabled: enabled, receivedAt: DateTime.now()),
            );
          });
        },
        onError: (Object error) {
          setState(() {
            _streamError = error is PlatformException
                ? error.message ?? error.code
                : '$error';
          });
        },
      );
    });
  }

  // Don't forget to cancel the subscription when done.
  Future<void> _stopListening() async {
    final subscription = _subscription;
    setState(() => _subscription = null);
    await subscription?.cancel();
  }

  // Launch the device's battery settings screen.
  Future<void> _launchBatterySettings() async {
    setState(() => _launchingSettings = true);
    String message;
    try {
      final success = await _plugin.launchBatterySettings();
      message = success
          ? 'Battery settings opened successfully'
          : 'Failed to open battery settings';
    } on PlatformException catch (e) {
      message = 'Failed to open battery settings: ${e.message ?? e.code}';
    }
    if (!mounted) {
      return;
    }
    setState(() => _launchingSettings = false);
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(message)));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('simplest_power_utilities')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: <Widget>[
          _StatusCard(
            enabled: _lowPowerModeEnabled,
            error: _statusError,
            loading: _loadingStatus,
            onRefresh: _refreshStatus,
          ),
          const SizedBox(height: 16),
          _BatteryLevelCard(
            level: _batteryLevel,
            error: _batteryError,
            loading: _loadingBatteryLevel,
            updatedAt: _batteryUpdatedAt,
            live: _batterySubscription != null,
            onRefresh: _refreshBatteryLevel,
            onLiveChanged: (bool live) {
              if (live) {
                _startBatteryUpdates();
              } else {
                _stopBatteryUpdates();
              }
            },
          ),
          const SizedBox(height: 16),
          _ChargingStateCard(
            state: _chargingState,
            error: _chargingError,
            loading: _loadingChargingState,
            updatedAt: _chargingUpdatedAt,
            live: _chargingSubscription != null,
            onRefresh: _refreshChargingState,
            onLiveChanged: (bool live) {
              if (live) {
                _startChargingUpdates();
              } else {
                _stopChargingUpdates();
              }
            },
          ),
          const SizedBox(height: 16),
          _ChangesCard(
            listening: _subscription != null,
            changes: _changes,
            error: _streamError,
            onListeningChanged: (bool listen) {
              if (listen) {
                _startListening();
              } else {
                _stopListening();
              }
            },
            onClear: () => setState(_changes.clear),
          ),
          const SizedBox(height: 16),
          _SettingsCard(
            launching: _launchingSettings,
            onLaunch: _launchBatterySettings,
          ),
        ],
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  const _StatusCard({
    required this.enabled,
    required this.error,
    required this.loading,
    required this.onRefresh,
  });

  final bool? enabled;
  final String? error;
  final bool loading;
  final VoidCallback onRefresh;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final Widget status;
    if (error != null) {
      status = Text(
        'Error: $error',
        key: const Key('status_error'),
        style: TextStyle(color: theme.colorScheme.error),
      );
    } else if (enabled == null) {
      status = const Text('Unknown', key: Key('status_unknown'));
    } else {
      status = Chip(
        key: const Key('status_chip'),
        avatar: Icon(
          enabled! ? Icons.battery_saver : Icons.battery_full,
          color: enabled! ? Colors.orange : Colors.green,
        ),
        label: Text(enabled! ? 'Enabled' : 'Disabled'),
      );
    }
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Low power mode', style: theme.textTheme.titleMedium),
            const SizedBox(height: 4),
            Text('isLowPowerModeEnabled()', style: theme.textTheme.bodySmall),
            const SizedBox(height: 12),
            Row(
              children: <Widget>[
                Expanded(child: status),
                FilledButton.tonalIcon(
                  key: const Key('refresh_button'),
                  onPressed: loading ? null : onRefresh,
                  icon: loading
                      ? const SizedBox.square(
                          dimension: 16,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Icon(Icons.refresh),
                  label: const Text('Refresh'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _BatteryLevelCard extends StatelessWidget {
  const _BatteryLevelCard({
    required this.level,
    required this.error,
    required this.loading,
    required this.updatedAt,
    required this.live,
    required this.onRefresh,
    required this.onLiveChanged,
  });

  final int? level;
  final String? error;
  final bool loading;
  final DateTime? updatedAt;
  final bool live;
  final VoidCallback onRefresh;
  final ValueChanged<bool> onLiveChanged;

  static IconData _iconFor(int level) {
    if (level >= 95) {
      return Icons.battery_full;
    }
    if (level >= 80) {
      return Icons.battery_6_bar;
    }
    if (level >= 65) {
      return Icons.battery_5_bar;
    }
    if (level >= 50) {
      return Icons.battery_4_bar;
    }
    if (level >= 35) {
      return Icons.battery_3_bar;
    }
    if (level >= 20) {
      return Icons.battery_2_bar;
    }
    if (level >= 10) {
      return Icons.battery_1_bar;
    }
    return Icons.battery_0_bar;
  }

  static Color _colorFor(int level) {
    if (level <= 20) {
      return Colors.red;
    }
    if (level <= 50) {
      return Colors.orange;
    }
    return Colors.green;
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final level = this.level;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Battery level', style: theme.textTheme.titleMedium),
            const SizedBox(height: 4),
            Text(
              'getBatteryLevel() / onBatteryLevelChanged',
              style: theme.textTheme.bodySmall,
            ),
            const SizedBox(height: 12),
            Row(
              children: <Widget>[
                if (level != null)
                  Icon(_iconFor(level), size: 40, color: _colorFor(level)),
                const SizedBox(width: 8),
                Expanded(
                  child: AnimatedSwitcher(
                    duration: const Duration(milliseconds: 300),
                    child: level == null
                        ? Text(
                            error == null ? 'Unknown' : '—',
                            key: const Key('battery_level_unknown'),
                            style: theme.textTheme.displaySmall,
                          )
                        : Text(
                            '$level%',
                            key: ValueKey<int>(level),
                            style: theme.textTheme.displaySmall,
                          ),
                  ),
                ),
                FilledButton.tonalIcon(
                  key: const Key('battery_refresh_button'),
                  onPressed: loading ? null : onRefresh,
                  icon: loading
                      ? const SizedBox.square(
                          dimension: 16,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Icon(Icons.refresh),
                  label: const Text('Refresh'),
                ),
              ],
            ),
            const SizedBox(height: 12),
            ClipRRect(
              borderRadius: BorderRadius.circular(4),
              child: LinearProgressIndicator(
                key: const Key('battery_level_bar'),
                minHeight: 8,
                value: level == null ? 0 : level / 100,
                color: level == null ? null : _colorFor(level),
              ),
            ),
            if (error != null) ...<Widget>[
              const SizedBox(height: 8),
              Text(
                error!,
                key: const Key('battery_error'),
                style: TextStyle(color: theme.colorScheme.error),
              ),
            ],
            SwitchListTile(
              key: const Key('battery_live_switch'),
              contentPadding: EdgeInsets.zero,
              title: const Text('Live updates'),
              subtitle: Text(
                updatedAt == null
                    ? (live ? 'Waiting for the first value…' : 'Not listening.')
                    : '${live ? 'Live' : 'Paused'} · updated ${_formatTime(updatedAt!)}',
                key: const Key('battery_status'),
              ),
              value: live,
              onChanged: onLiveChanged,
            ),
          ],
        ),
      ),
    );
  }
}

class _ChargingStateCard extends StatelessWidget {
  const _ChargingStateCard({
    required this.state,
    required this.error,
    required this.loading,
    required this.updatedAt,
    required this.live,
    required this.onRefresh,
    required this.onLiveChanged,
  });

  final ChargingState? state;
  final String? error;
  final bool loading;
  final DateTime? updatedAt;
  final bool live;
  final VoidCallback onRefresh;
  final ValueChanged<bool> onLiveChanged;

  static String _statusLabel(ChargingStatus status) => switch (status) {
    ChargingStatus.charging => 'Charging',
    ChargingStatus.full => 'Full',
    ChargingStatus.notCharging => 'Plugged in, not charging',
    ChargingStatus.discharging => 'On battery',
    ChargingStatus.unknown => 'Unknown',
  };

  static String _sourceLabel(ChargingSource source) => switch (source) {
    ChargingSource.ac => 'AC adapter',
    ChargingSource.usb => 'USB',
    ChargingSource.wireless => 'Wireless',
    ChargingSource.dock => 'Dock',
    ChargingSource.none => 'Not plugged in',
    ChargingSource.unknown => 'Unknown source',
  };

  static IconData _sourceIcon(ChargingSource source) => switch (source) {
    ChargingSource.ac => Icons.power,
    ChargingSource.usb => Icons.usb,
    ChargingSource.wireless => Icons.wifi_tethering,
    ChargingSource.dock => Icons.dock,
    ChargingSource.none => Icons.power_off,
    ChargingSource.unknown => Icons.help_outline,
  };

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final state = this.state;
    final Color statusColor = state == null
        ? theme.colorScheme.onSurface
        : state.isCharging
        ? Colors.green
        : state.status == ChargingStatus.notCharging
        ? Colors.orange
        : theme.colorScheme.onSurface;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Charging state', style: theme.textTheme.titleMedium),
            const SizedBox(height: 4),
            Text(
              'getChargingState() / onChargingStateChanged',
              style: theme.textTheme.bodySmall,
            ),
            const SizedBox(height: 12),
            Row(
              children: <Widget>[
                Icon(
                  state?.isCharging ?? false ? Icons.bolt : Icons.battery_std,
                  size: 40,
                  color: statusColor,
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: AnimatedSwitcher(
                    duration: const Duration(milliseconds: 300),
                    child: Column(
                      key: ValueKey<ChargingState?>(state),
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          state == null
                              ? 'Unknown'
                              : _statusLabel(state.status),
                          key: const Key('charging_status'),
                          style: theme.textTheme.titleLarge?.copyWith(
                            color: statusColor,
                          ),
                        ),
                        if (state != null)
                          Chip(
                            key: const Key('charging_source'),
                            avatar: Icon(_sourceIcon(state.source), size: 18),
                            label: Text(_sourceLabel(state.source)),
                            visualDensity: VisualDensity.compact,
                          ),
                      ],
                    ),
                  ),
                ),
                FilledButton.tonalIcon(
                  key: const Key('charging_refresh_button'),
                  onPressed: loading ? null : onRefresh,
                  icon: loading
                      ? const SizedBox.square(
                          dimension: 16,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Icon(Icons.refresh),
                  label: const Text('Refresh'),
                ),
              ],
            ),
            if (error != null) ...<Widget>[
              const SizedBox(height: 8),
              Text(
                error!,
                key: const Key('charging_error'),
                style: TextStyle(color: theme.colorScheme.error),
              ),
            ],
            SwitchListTile(
              key: const Key('charging_live_switch'),
              contentPadding: EdgeInsets.zero,
              title: const Text('Live updates'),
              subtitle: Text(
                updatedAt == null
                    ? (live ? 'Waiting for the first value…' : 'Not listening.')
                    : '${live ? 'Live' : 'Paused'} · updated ${_formatTime(updatedAt!)}',
                key: const Key('charging_status_line'),
              ),
              value: live,
              onChanged: onLiveChanged,
            ),
          ],
        ),
      ),
    );
  }
}

class _ChangesCard extends StatelessWidget {
  const _ChangesCard({
    required this.listening,
    required this.changes,
    required this.error,
    required this.onListeningChanged,
    required this.onClear,
  });

  final bool listening;
  final List<LowPowerModeChange> changes;
  final String? error;
  final ValueChanged<bool> onListeningChanged;
  final VoidCallback onClear;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Real-time updates', style: theme.textTheme.titleMedium),
            const SizedBox(height: 4),
            Text('onLowPowerModeChanged', style: theme.textTheme.bodySmall),
            SwitchListTile(
              key: const Key('listen_switch'),
              contentPadding: EdgeInsets.zero,
              title: const Text('Listen for changes'),
              subtitle: Text(
                listening
                    ? 'Toggle low power mode in the system settings to see events.'
                    : 'Subscription is not active.',
              ),
              value: listening,
              onChanged: onListeningChanged,
            ),
            if (error != null)
              Text(
                'Stream error: $error',
                key: const Key('stream_error'),
                style: TextStyle(color: theme.colorScheme.error),
              ),
            if (changes.isEmpty)
              const Text('No changes received yet.', key: Key('no_changes'))
            else ...<Widget>[
              for (final change in changes)
                ListTile(
                  contentPadding: EdgeInsets.zero,
                  dense: true,
                  leading: Icon(
                    change.enabled ? Icons.battery_saver : Icons.battery_full,
                    color: change.enabled ? Colors.orange : Colors.green,
                  ),
                  title: Text(
                    'Low power mode ${change.enabled ? 'enabled' : 'disabled'}',
                  ),
                  subtitle: Text(_formatTime(change.receivedAt)),
                ),
              Align(
                alignment: Alignment.centerRight,
                child: TextButton(
                  key: const Key('clear_button'),
                  onPressed: onClear,
                  child: const Text('Clear'),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _SettingsCard extends StatelessWidget {
  const _SettingsCard({required this.launching, required this.onLaunch});

  final bool launching;
  final VoidCallback onLaunch;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Battery settings', style: theme.textTheme.titleMedium),
            const SizedBox(height: 4),
            Text('launchBatterySettings()', style: theme.textTheme.bodySmall),
            const SizedBox(height: 12),
            FilledButton.icon(
              key: const Key('launch_settings_button'),
              onPressed: launching ? null : onLaunch,
              icon: const Icon(Icons.settings),
              label: const Text('Open battery settings'),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
504
downloads

Documentation

API reference

Publisher

verified publisherbizjak.dev

Weekly Downloads

Plugin which exposes various power-related utilities for Android and iOS, including low power mode detection

Repository (GitLab)

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on simplest_power_utilities

Packages that implement simplest_power_utilities