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

CauseImpactFix
GPS with high accuracy + zero distance filterVery HighUse
text
LocationAccuracy.low
+
text
distanceFilter: 100
Timers not disposedHighCancel in
text
dispose()
Animation controllers not disposedHighDispose in
text
dispose()
Frequent network pollingMedium-HighDebounce, batch requests
WebSocket always openHighClose on app background
Stream subscriptions not cancelledHighCancel in
text
dispose()
Wake locks held unnecessarilyHighRelease when not needed
Unoptimized images (no caching)MediumUse
text
cached_network_image
Unnecessary widget rebuildsMediumUse
text
const
,
text
RepaintBoundary

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)

text
1. 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

ActionImpact
Cancel all
text
Timer
/
text
Timer.periodic
in
text
dispose()
High
Dispose all
text
AnimationController
instances
High
Cancel all
text
StreamSubscription
in
text
dispose()
High
Use
text
LocationAccuracy.low
+
text
distanceFilter: 100
Very High
Use
text
getLastKnownPosition()
when fresh position not needed
High
Debounce search/input-driven API callsMedium
Use
text
cached_network_image
with size constraints
Medium
Use
text
workmanager
instead of
text
Timer.periodic
for background tasks
Very High
Close WebSocket on
text
AppLifecycleState.paused
High
Use
text
const
constructors and
text
RepaintBoundary
Medium
Avoid broad
text
setState()
— isolate state changes
Medium

Platform-Specific Tools Summary

ToolPlatformWhat It Shows
text
adb shell dumpsys batterystats
AndroidPer-app battery usage breakdown
text
adb shell dumpsys power
AndroidWake locks held
Android Studio Energy ProfilerAndroidCPU, network, GPS, wake lock timeline
Perfetto (
text
ui.perfetto.dev
)
AndroidDetailed power rail tracing
Xcode Instruments Energy LogiOSCPU, network, location, GPU energy
Xcode Debug Navigator > EnergyiOSReal-time energy impact gauge
Flutter DevToolsBothCPU profiler, memory, network, rebuilds

Key Rule: Always

text
dispose()
what you
text
init()
. If you create a timer, controller, subscription, or connection in
text
initState()
, you must clean it up in
text
dispose()
. This is the single most impactful habit for preventing battery drain.