battery_ffi 0.0.1
battery_ffi: ^0.0.1 copied to clipboard
A comprehensive Flutter plugin for real-time battery monitoring across platforms using FFI/JNI for optimal performance.
Battery FFI #
A comprehensive Flutter plugin for real-time battery monitoring across platforms using FFI (Foreign Function Interface) and JNI (Java Native Interface) for optimal performance.
🚀 Features #
- Real-time battery monitoring with live stream updates
- Cross-platform support (Android, iOS, Windows, macOS, Linux, Web)
- High-performance FFI/JNI implementation for direct native access
- Comprehensive battery data including level, state, health, temperature, voltage
- Charging information with current, speed, and type detection
- Data source transparency (API vs estimated values)
- Robust error handling with detailed recovery suggestions
- Resource management with automatic cleanup
- Battery saver mode detection
- Stream-based architecture for reactive programming
📦 Installation #
Basic Setup #
Add this to your package's pubspec.yaml file:
dependencies:
battery_ffi: ^0.0.1
Then run:
flutter pub get
Android Setup #
Minimum Requirements:
- Android API level 21+ (Android 5.0)
- No additional permissions required
- Standard Android BatteryManager APIs
Optional: Background Monitoring
For continuous battery monitoring when the app is backgrounded or screen is off:
// Acquire wake lock for continuous monitoring
BatteryFFI.instance.acquireWakeLock();
// Your battery monitoring code here...
// Always release wake lock when done to save battery
BatteryFFI.instance.releaseWakeLock();
⚠️ Important: Wake locks prevent device sleep. Always release them promptly to avoid battery drain.
iOS Setup #
No additional setup required. iOS permissions are handled automatically.
Note: iOS provides limited battery information due to platform restrictions.
Desktop Setup (Windows/macOS/Linux) #
No additional setup required. Desktop platforms provide basic battery information where available.
Web Setup #
Limited support using the deprecated Battery Status API. Consider using alternative approaches for web applications.
🔧 Platform-Specific Configuration #
Android Manifest (Optional) #
If you need to declare wake lock usage for Play Store transparency:
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Optional: Declare wake lock usage -->
<application>
<meta-data
android:name="android.suppressUnsupportedCompileSdk"
android:value="34" />
</application>
iOS Info.plist (Optional) #
<!-- ios/Runner/Info.plist -->
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
</array>
Build Configuration #
Ensure your android/app/build.gradle has:
android {
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
}
}
📚 Quick Start #
What Does initialize() Do? #
The initialize() method performs platform-specific setup to prepare the battery monitoring system:
On Android:
- Loads JNI (Java Native Interface) libraries
- Registers Android
BroadcastReceiverfor battery change events - Sets up JNI callback mechanisms for real-time updates
- Resets any stale state from previous app runs
- Initializes wake lock management system
On iOS:
- Sets up system battery monitoring APIs
- Configures background processing permissions
- Prepares for battery state change notifications
On Desktop/Web:
- Initializes platform-specific battery APIs
- Sets up fallback mechanisms for unsupported features
Why Required:
- Ensures proper resource allocation before use
- Prevents crashes from uninitialized JNI callbacks
- Sets up platform-specific monitoring infrastructure
- Must be called before accessing any battery properties
Basic Usage #
import 'package:battery_ffi/battery_ffi.dart';
void main() async {
// Initialize the battery monitor (REQUIRED)
await BatteryFFI.instance.initialize();
// Now you can safely access battery information
final level = BatteryFFI.instance.batteryLevel;
print('Battery Level: $level%');
final state = BatteryFFI.instance.batteryState;
print('Battery State: ${state?.displayName}');
// Clean up when done
BatteryFFI.instance.dispose();
}
Real-time Monitoring with Streams #
import 'package:battery_ffi/battery_ffi.dart';
class BatteryMonitorWidget extends StatefulWidget {
@override
_BatteryMonitorWidgetState createState() => _BatteryMonitorWidgetState();
}
class _BatteryMonitorWidgetState extends State<BatteryMonitorWidget> {
late StreamSubscription<BatteryState> _stateSubscription;
late StreamSubscription<bool> _chargerSubscription;
BatteryState? _currentState;
bool _isCharging = false;
@override
void initState() {
super.initState();
_initializeBatteryMonitoring();
}
Future<void> _initializeBatteryMonitoring() async {
// Initialize battery monitoring
await BatteryFFI.instance.initialize();
// Listen to battery state changes
_stateSubscription = BatteryFFI.instance.onBatteryStateChanged.listen(
(state) => setState(() => _currentState = state),
);
// Listen to charger connection changes
_chargerSubscription = BatteryFFI.instance.onChargerConnectionChanged.listen(
(isConnected) => setState(() => _isCharging = isConnected),
);
}
@override
void dispose() {
_stateSubscription.cancel();
_chargerSubscription.cancel();
BatteryFFI.instance.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Battery State: ${_currentState?.displayName ?? 'Unknown'}'),
Text('Charging: ${_isCharging ? 'Yes' : 'No'}'),
Text('Level: ${BatteryFFI.instance.batteryLevel}%'),
],
);
}
}
Advanced Battery Information #
import 'package:battery_ffi/battery_ffi.dart';
Future<void> printDetailedBatteryInfo() async {
final battery = BatteryFFI.instance;
print('=== Battery Information ===');
print('Level: ${battery.batteryLevel}%');
print('State: ${battery.batteryState?.displayName}');
print('Health: ${battery.batteryHealth?.displayName}');
print('Temperature: ${battery.batteryTemperature}°C');
print('Voltage: ${battery.batteryVoltage}V');
print('Charging Current: ${battery.maxChargingCurrent}mA');
print('Battery Capacity: ${battery.batteryCapacity}μAh');
print('Charging Speed: ${battery.chargingSpeed}W');
print('Charging Type: ${battery.chargingType}');
// Data source indicators
print('Current from API: ${battery.isChargingCurrentFromApi}');
print('Capacity from API: ${battery.isBatteryCapacityFromApi}');
// Battery saver mode (sync)
final isSaverMode = battery.isInBatterySaveMode;
print('Battery Saver Mode: $isSaverMode');
}
Error Handling #
import 'package:battery_ffi/battery_ffi.dart';
Future<void> safeBatteryMonitoring() async {
try {
await BatteryFFI.instance.initialize();
final level = BatteryFFI.instance.batteryLevel;
print('Battery level: $level%');
} on BatteryException catch (e) {
print('Battery monitoring error: ${e.message}');
print('Error type: ${e.type.description}');
if (e.recoverySuggestion != null) {
print('Recovery suggestion: ${e.recoverySuggestion}');
}
// Handle specific error types
switch (e.type) {
case BatteryErrorType.unsupportedPlatform:
// Show platform not supported message
break;
case BatteryErrorType.permissionDenied:
// Request permissions or show settings
break;
case BatteryErrorType.apiUnavailable:
// Fall back to alternative implementation
break;
default:
// Handle other errors
break;
}
} catch (e) {
print('Unexpected error: $e');
} finally {
BatteryFFI.instance.dispose();
}
}
Performance Optimization #
import 'package:battery_ffi/battery_ffi.dart';
class OptimizedBatteryWidget extends StatefulWidget {
@override
_OptimizedBatteryWidgetState createState() => _OptimizedBatteryWidgetState();
}
class _OptimizedBatteryWidgetState extends State<OptimizedBatteryWidget> {
Timer? _rapidUpdateTimer;
@override
void initState() {
super.initState();
_startOptimizedMonitoring();
}
void _startOptimizedMonitoring() {
// For rapid updates, pause streams to prevent overload
BatteryFFI.instance.pauseStreams();
// Use timer for controlled updates
_rapidUpdateTimer = Timer.periodic(Duration(milliseconds: 100), (_) {
setState(() {
// Direct property access is faster than streams for rapid updates
final level = BatteryFFI.instance.batteryLevel;
final voltage = BatteryFFI.instance.batteryVoltage;
// Update UI with latest values
});
});
}
@override
void dispose() {
_rapidUpdateTimer?.cancel();
BatteryFFI.instance.resumeStreams(); // Resume normal streaming
BatteryFFI.instance.dispose();
super.dispose();
}
}
🎯 API Reference #
BatteryFFI (Main Class) #
The main battery monitoring interface with singleton access.
// Get the platform-specific instance
BatteryFFI battery = BatteryFFI.instance;
// Initialize monitoring
await battery.initialize();
// Clean up resources
battery.dispose();
Synchronous Properties #
| Property | Type | Description |
|---|---|---|
batteryLevel |
int |
Current battery level (0-100%) |
batteryState |
BatteryState? |
Current charging state |
batteryHealth |
BatteryHealth? |
Battery health status |
isChargerConnected |
bool |
Whether charger is connected |
batteryTemperature |
double |
Temperature in Celsius |
batteryVoltage |
double |
Voltage in Volts |
maxChargingCurrent |
int |
Maximum charging current (mA) |
batteryCapacity |
int |
Battery capacity (μAh) |
chargingSpeed |
int |
Current charging speed (W) |
chargingType |
String |
Charger type (AC, USB, etc.) |
Data Source Indicators #
| Property | Type | Description |
|---|---|---|
isChargingCurrentFromApi |
bool |
Whether current value is from API |
isChargingSpeedFromApi |
bool |
Whether speed value is from API |
isBatteryCapacityFromApi |
bool |
Whether capacity value is from API |
Synchronous Properties #
| Property | Type | Description |
|---|---|---|
isInBatterySaveMode |
bool |
Battery saver mode status |
Stream Properties #
| Stream | Type | Description |
|---|---|---|
onBatteryStateChanged |
Stream<BatteryState> |
Battery state changes |
onBatterySaverModeChanged |
Stream<bool> |
Battery saver mode changes |
onBatteryHealthChanged |
Stream<BatteryHealth> |
Battery health changes |
onChargerConnectionChanged |
Stream<bool> |
Charger connection changes |
Utility Methods #
| Method | Description |
|---|---|
pauseStreams() |
Temporarily pause stream updates |
resumeStreams() |
Resume stream updates |
acquireWakeLock() |
Acquire wake lock for continuous monitoring (Android only) |
releaseWakeLock() |
Release wake lock (Android only) |
isWakeLockHeld |
Check if wake lock is currently held (Android only) |
BatteryState Enum #
enum BatteryState {
charging, // Battery is charging
discharging, // Battery is discharging
notCharging, // Plugged in but not charging
full, // Battery is fully charged
unknown // State is unknown
}
// Usage
final state = BatteryFFI.instance.batteryState;
print(state?.displayName); // "Charging"
print(state?.iconName); // "battery_charging_full"
BatteryHealth Enum #
enum BatteryHealth {
good, // Battery is healthy
overheat, // Battery is overheating
dead, // Battery is dead
overVoltage, // Over voltage condition
unspecifiedFailure,// Unspecified failure
cold, // Battery is cold
unknown // Health status unknown
}
// Usage
final health = BatteryFFI.instance.batteryHealth;
print(health?.displayName); // "Good"
print(health?.colorName); // "green"
print(health?.isProblematic); // false
BatteryException #
Custom exception class with detailed error information.
try {
await BatteryFFI.instance.initialize();
} on BatteryException catch (e) {
print('Error: ${e.message}');
print('Type: ${e.type.description}');
print('Recovery: ${e.recoverySuggestion}');
}
🔧 Best Practices #
1. Initialization and Cleanup #
Always initialize the battery monitor and clean up resources:
@override
void initState() {
super.initState();
BatteryFFI.instance.initialize();
}
@override
void dispose() {
BatteryFFI.instance.dispose();
super.dispose();
}
2. Stream Management #
Cancel stream subscriptions to prevent memory leaks:
late StreamSubscription _subscription;
@override
void initState() {
super.initState();
_subscription = BatteryFFI.instance.onBatteryStateChanged.listen((state) {
// Handle state changes
});
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
3. Error Handling #
Implement comprehensive error handling:
try {
final level = BatteryFFI.instance.batteryLevel;
} on BatteryException catch (e) {
// Handle battery-specific errors
switch (e.type) {
case BatteryErrorType.permissionDenied:
// Request permissions
break;
case BatteryErrorType.unsupportedPlatform:
// Show fallback UI
break;
default:
// Handle other errors
break;
}
}
4. Performance Considerations #
- Use streams for real-time updates
- Use direct property access for one-time reads
- Pause streams during rapid operations
- Consider battery impact on mobile devices
5. Platform-Specific Behavior #
Different platforms may provide different levels of information:
- Android: Full battery information via JNI
- iOS: Limited information due to platform restrictions
- Desktop: Basic information where available
- Web: Limited support (deprecated Battery Status API)
🚨 Common Errors & Troubleshooting #
Android Issues #
"Callback invoked after it has been deleted" Crash
Symptoms:
- App crashes with
SIGABRTon startup or restart - Error: "Callback invoked after it has been deleted"
- Fatal signal 6 in JNI layer
Cause: JNI callbacks being invoked after Dart objects are garbage collected.
Solution: This is automatically handled by the plugin. No user action required.
Prevention:
- Always call
BatteryFFI.instance.dispose()in app lifecycle - The plugin automatically resets state on app restart
"JNI ERROR (app bug): accessed stale global reference" Warning
Symptoms:
- Warning messages in Android logs
- No functional impact but concerning logs
Cause: Improper JNI reference management.
Solution: This is automatically handled by the plugin. The warning is harmless.
Wake Lock Not Working
Symptoms:
acquireWakeLock()doesn't prevent app suspension- Background monitoring stops when screen turns off
Cause:
- Wake lock released too early
- Android battery optimization killing the app
- Incorrect wake lock usage
Solutions:
// 1. Ensure proper wake lock lifecycle
BatteryFFI.instance.acquireWakeLock();
// ... do background work ...
BatteryFFI.instance.releaseWakeLock();
// 2. Check if wake lock was acquired
if (BatteryFFI.instance.isWakeLockHeld) {
print('Wake lock active');
}
// 3. For Android 6.0+, check battery optimization
// Go to Settings > Apps > Your App > Battery > Don't optimize
Battery Data Shows -1 or Incorrect Values
Symptoms:
- Battery level shows -1
- Temperature/voltage readings are wrong
- Charging information unavailable
Cause:
- Device doesn't support certain battery APIs
- Battery service temporarily unavailable
- Android version compatibility issues
Solutions:
// Check data source indicators
final battery = BatteryFFI.instance;
print('Current from API: ${battery.isChargingCurrentFromApi}');
print('Capacity from API: ${battery.isBatteryCapacityFromApi}');
// Handle unavailable data gracefully
final level = battery.batteryLevel;
if (level == -1) {
print('Battery level unavailable');
} else {
print('Battery level: $level%');
}
iOS Issues #
Limited Battery Information
Symptoms:
- Only basic battery level available
- No temperature, voltage, or detailed charging info
Cause: iOS platform restrictions limit battery API access.
Solution: This is expected behavior. iOS provides minimal battery information for privacy reasons.
// iOS will only provide basic information
final level = BatteryFFI.instance.batteryLevel; // ✅ Works
final temp = BatteryFFI.instance.batteryTemperature; // ❌ Limited/unavailable
General Issues #
"Platform not supported" Error
Symptoms:
BatteryExceptionwithBatteryErrorType.unsupportedPlatform- Plugin fails to initialize
Cause: Running on an unsupported platform or missing platform implementation.
Solutions:
try {
await BatteryFFI.instance.initialize();
} on BatteryException catch (e) {
if (e.type == BatteryErrorType.unsupportedPlatform) {
// Show platform not supported message
print('Battery monitoring not available on this platform');
// Provide fallback UI or disable battery features
}
}
Memory Leaks
Symptoms:
- App memory usage increases over time
- Performance degradation during long battery monitoring sessions
Cause: Not properly canceling stream subscriptions or disposing resources.
Solutions:
class BatteryMonitor extends State<StatefulWidget> {
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_subscription = BatteryFFI.instance.onBatteryStateChanged.listen((state) {
// Handle updates
});
}
@override
void dispose() {
_subscription?.cancel(); // Always cancel subscriptions
BatteryFFI.instance.dispose(); // Clean up resources
super.dispose();
}
}
Stream Not Updating
Symptoms:
- Stream listeners not receiving updates
- UI not refreshing with new battery data
Cause:
- Streams paused but not resumed
- Subscription canceled accidentally
- Platform-specific stream limitations
Solutions:
// Check if streams are paused
BatteryFFI.instance.resumeStreams();
// Recreate subscription if needed
_subscription = BatteryFFI.instance.onBatteryStateChanged.listen((state) {
setState(() => _currentState = state);
});
// For Android background limitations
if (Platform.isAndroid) {
// Use wake locks for background monitoring
BatteryFFI.instance.acquireWakeLock();
}
Compilation Errors
Symptoms:
- Build fails with JNI-related errors
- Missing symbol errors
- Gradle build issues
Common Causes & Solutions:
- Missing JniGen Generation:
# Regenerate JNI bindings
flutter pub run jnigen --config jnigen.yaml
- Android NDK Issues:
// android/app/build.gradle
android {
ndkVersion "25.1.8937393" // Use compatible NDK version
}
- ProGuard/R8 Issues:
// android/app/build.gradle
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
# proguard-rules.pro
-keep class com.example.battery_ffi.** { *; }
-keep class battery_ffi.** { *; }
Performance Issues #
High CPU Usage
Symptoms:
- Battery drains faster than expected
- Device gets hot during monitoring
- App uses excessive CPU
Solutions:
// 1. Pause streams during rapid operations
BatteryFFI.instance.pauseStreams();
// Perform rapid updates...
BatteryFFI.instance.resumeStreams();
// 2. Use direct property access instead of streams for one-time reads
final level = BatteryFFI.instance.batteryLevel; // Faster than streams
// 3. Limit update frequency
Timer.periodic(Duration(seconds: 30), (_) {
// Update every 30 seconds instead of real-time
final level = BatteryFFI.instance.batteryLevel;
setState(() => _batteryLevel = level);
});
UI Freezing
Symptoms:
- App becomes unresponsive during battery operations
- Main thread blocked
Cause: Heavy operations on main thread.
Solutions:
// Use async operations off main thread
Future<void> _updateBatteryInfo() async {
final level = await compute((_) => BatteryFFI.instance.batteryLevel, null);
setState(() => _batteryLevel = level);
}
❓ FAQ #
Q: Why do I need to call initialize()? #
A: The initialize() method sets up platform-specific resources (JNI on Android, system APIs on other platforms). It ensures proper resource allocation and prepares the monitoring system.
Q: Can I use this plugin in background services? #
A: On Android, yes - use wake locks for continuous monitoring. On iOS and other platforms, background monitoring is limited by platform restrictions.
Q: Why are some values -1 or unavailable? #
A: Some battery information may not be available on certain devices or Android versions. The plugin provides data source indicators to show when values are estimated vs from APIs.
Q: How often do streams update? #
A: Streams update when Android's ACTION_BATTERY_CHANGED broadcast is fired, typically when battery level changes by 1% or charging state changes.
Q: Is this plugin compatible with Flutter's null safety? #
A: Yes, the plugin is fully null-safe and requires Dart 2.12+.
Q: Can I use multiple instances simultaneously? #
A: No, use the singleton BatteryFFI.instance. Multiple instances would conflict with JNI resources.
Q: What happens if I forget to call dispose()? #
A: Resources may not be cleaned up properly, potentially causing memory leaks or continued battery drain from wake locks.
Q: Why are wake locks Android-only? #
A: Wake locks are an Android-specific power management feature. Other platforms handle background processing differently.
Q: Can I monitor battery temperature? #
A: Yes, on supported devices. Check BatteryFFI.instance.batteryTemperature (returns Celsius).
Q: How accurate are the charging current readings? #
A: Accuracy depends on device and Android version. Use isChargingCurrentFromApi to check if the value comes from device APIs or is estimated.
🏗️ Architecture #
The library uses a platform-specific implementation pattern:
BatteryFFI (Abstract Interface)
├── AndroidBatteryMonitor (Android - JNI)
├── StubBatteryMonitor (Fallback)
└── WebBatteryMonitor (Web - Limited)
📋 Platform Support #
| Platform | Status | Implementation | Features |
|---|---|---|---|
| Android | ✅ Complete | JNI with BroadcastReceiver | Full battery monitoring |
| iOS | 🚧 Planned | IOKit framework | Limited monitoring |
| Windows | 🚧 Planned | Windows API | Basic monitoring |
| macOS | 🚧 Planned | IOKit framework | Basic monitoring |
| Linux | 🚧 Planned | UPower/DBus | Basic monitoring |
| Web | ⚠️ Limited | Battery Status API | Minimal support |
🤝 Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License #
This project is licensed under the MIT License.
Copyright (c) 2025 Shreeman Arjun Sahu
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
See the LICENSE file for the complete license text.