When the user says the Flutter app is draining a lot of battery, what to do? How to analyse proactively, how to avoid it, and how to debug it?
#flutter#battery#performance#optimization#debugging
Answer
Overview
Battery drain in Flutter apps is usually caused by unmanaged background processes, excessive GPS usage, undisposed timers/controllers, and frequent network calls. Here's a complete guide to analyse, debug, and fix battery issues.
Common Causes of Battery Drain
| Cause | Impact | Fix |
|---|---|---|
| GPS with high accuracy + zero distance filter | Very High | Use text text |
| Timers not disposed | High | Cancel in text |
| Animation controllers not disposed | High | Dispose in text |
| Frequent network polling | Medium-High | Debounce, batch requests |
| WebSocket always open | High | Close on app background |
| Stream subscriptions not cancelled | High | Cancel in text |
| Wake locks held unnecessarily | High | Release when not needed |
| Unoptimized images (no caching) | Medium | Use text |
| Unnecessary widget rebuilds | Medium | Use text text |
Step-by-Step Debugging Guide
Step 1: Establish a Baseline
bash# Reset battery stats adb shell dumpsys batterystats --reset # Check current battery level adb shell dumpsys battery | grep level
Leave device idle for 30 minutes without your app. Record battery drop — this is your baseline.
Step 2: Measure With Your App
bash# Reset stats again adb shell dumpsys batterystats --reset # Use the app normally for 30 minutes, then: adb shell dumpsys batterystats --charged com.example.myapp
Compare battery drop vs baseline. Repeat with app in background to check background drain.
Step 3: Check Wake Locks, GPS, Network
bash# Check wake locks held by your app adb shell dumpsys power | grep "Wake Locks" # Check GPS usage adb shell dumpsys location | grep com.example.myapp # Check network stats adb shell dumpsys netstats detail | grep com.example.myapp
Step 4: Use Flutter DevTools
- CPU Profiler — Identify hot functions consuming excessive CPU
- Performance Overlay — Spot jank and excessive frame rendering
- Memory tab — Detect leaks from undisposed controllers/timers
- Network tab — Monitor frequency and size of network requests
Step 5: iOS Debugging (Xcode)
text1. Xcode > Open Developer Tool > Instruments 2. Choose "Energy Log" template 3. Select your physical iOS device + Flutter app 4. Click Record and exercise the app 5. Analyse: CPU (blue bars), Network, Location, GPU sections
How to Fix — Bad vs Good Patterns
1. Timer Not Disposed vs Properly Disposed
dart// ❌ BAD — Timer keeps firing after widget removed class _MyWidgetState extends State<MyWidget> { Timer? _timer; void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 5), (timer) { _fetchData(); // Runs forever! }); } // Missing dispose()! }
dart// ✅ GOOD — Timer cancelled in dispose class _MyWidgetState extends State<MyWidget> { Timer? _timer; void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 5), (timer) { if (mounted) _fetchData(); }); } void dispose() { _timer?.cancel(); _timer = null; super.dispose(); } }
2. Animation Controller Not Disposed
dart// ❌ BAD — Animation ticks at 60fps forever class _AnimState extends State<AnimWidget> { late AnimationController _controller; void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); // Runs forever! } // Missing dispose()! }
dart// ✅ GOOD — Disposed + RepaintBoundary class _AnimState extends State<AnimWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); } Widget build(BuildContext context) { return RepaintBoundary( child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.rotate( angle: _controller.value * 2 * 3.14159, child: child, ); }, child: const FlutterLogo(size: 100), ), ); } void dispose() { _controller.dispose(); super.dispose(); } }
3. GPS — High Power vs Battery Optimized
dart// ❌ BAD — GPS at max accuracy, updates every meter _positionSub = Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 0, ), ).listen((position) { _sendToServer(position); // Network call on every tiny move! });
dart// ✅ GOOD — Low accuracy, large distance filter _positionSub = Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, // Cell towers/WiFi, not GPS distanceFilter: 100, // Only update when moved 100m ), ).listen((position) { if (_shouldSendUpdate(position)) { _sendToServer(position); } }); // Use cached position when fresh one isn't needed final cached = await Geolocator.getLastKnownPosition(); void dispose() { _positionSub?.cancel(); super.dispose(); }
4. WebSocket — Close on Background
dart// ✅ GOOD — Lifecycle-aware WebSocket class _ChatState extends State<ChatScreen> with WidgetsBindingObserver { WebSocketChannel? _channel; void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _connect(); } void _connect() { _channel = WebSocketChannel.connect( Uri.parse('wss://example.com/ws'), ); } void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { _channel?.sink.close(); // Save battery when backgrounded _channel = null; } else if (state == AppLifecycleState.resumed) { _connect(); // Reconnect when foregrounded } } void dispose() { WidgetsBinding.instance.removeObserver(this); _channel?.sink.close(); super.dispose(); } }
5. Background Tasks — WorkManager vs Timer
dart// ❌ BAD — Timer dies in background, wastes battery in foreground Timer.periodic(const Duration(minutes: 15), (timer) { syncDataToServer(); });
dart// ✅ GOOD — WorkManager (OS-optimized scheduling) import 'package:workmanager/workmanager.dart'; ('vm:entry-point') void callbackDispatcher() { Workmanager().executeTask((task, inputData) async { await _syncPendingData(); return true; }); } void setupBackgroundSync() { Workmanager().initialize(callbackDispatcher); Workmanager().registerPeriodicTask( 'periodic-sync', 'com.example.syncData', frequency: const Duration(hours: 1), constraints: Constraints( networkType: NetworkType.connected, requiresBatteryNotLow: true, // Skip when battery low ), ); }
6. Debounce Network Calls
dart// ✅ GOOD — Debounced search class _SearchState extends State<SearchPage> { Timer? _debounce; void _onSearchChanged(String query) { _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () { if (query.length >= 3) ApiService.search(query); }); } void dispose() { _debounce?.cancel(); super.dispose(); } }
Proactive Prevention Checklist
| Action | Impact |
|---|---|
| Cancel all text text text | High |
| Dispose all text | High |
| Cancel all text text | High |
| Use text text | Very High |
| Use text | High |
| Debounce search/input-driven API calls | Medium |
| Use text | Medium |
| Use text text | Very High |
| Close WebSocket on text | High |
| Use text text | Medium |
| Avoid broad text | Medium |
Platform-Specific Tools Summary
| Tool | Platform | What It Shows |
|---|---|---|
text | Android | Per-app battery usage breakdown |
text | Android | Wake locks held |
| Android Studio Energy Profiler | Android | CPU, network, GPS, wake lock timeline |
| Perfetto ( text | Android | Detailed power rail tracing |
| Xcode Instruments Energy Log | iOS | CPU, network, location, GPU energy |
| Xcode Debug Navigator > Energy | iOS | Real-time energy impact gauge |
| Flutter DevTools | Both | CPU profiler, memory, network, rebuilds |
Key Rule: Always
what youtextdispose(). If you create a timer, controller, subscription, or connection intextinit(), you must clean it up intextinitState(). This is the single most impactful habit for preventing battery drain.textdispose()