simplest_power_utilities 1.1.0
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
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'),
),
],
),
),
);
}
}