Handling Battery Optimization for Background Tasks in Flutter

Mobile operating systems like Android and iOS aggressively manage battery usage to improve the user experience. One of the strategies employed is battery optimization, which restricts background tasks to prolong battery life. Flutter developers need to understand how these optimizations affect their apps, especially those relying on background processing.

What is Battery Optimization?

Battery optimization is a set of techniques used by mobile operating systems to reduce battery consumption. These techniques typically involve limiting background activities, network access, and CPU usage when the device is not actively being used.

Why is Battery Optimization Important?

  • User Experience: Longer battery life improves user satisfaction.
  • System Health: Prevents apps from unnecessarily draining resources.
  • Environmental Impact: Reduces energy consumption.

Challenges with Background Tasks in Flutter

Flutter apps that perform background tasks, such as data syncing, location tracking, or push notification handling, may face challenges due to battery optimization:

  • Task Interruption: Background tasks may be terminated or delayed.
  • Inconsistent Behavior: Behavior can vary across different devices and OS versions.
  • Limited Functionality: Restrictions on network and CPU usage.

How to Handle Battery Optimization in Flutter

Here’s a comprehensive guide on handling battery optimization in Flutter to ensure your background tasks function reliably.

1. Understanding Android Doze Mode and App Standby Buckets

Android introduces Doze mode and App Standby Buckets, which affect how apps behave when the device is idle.

  • Doze Mode: Activates when the device is unplugged and stationary for a period. It defers background tasks, network access, and syncs.
  • App Standby Buckets: Places apps into different buckets based on usage patterns, limiting their resources. The buckets are:
    • Active
    • Working Set
    • Frequent
    • Rare
    • Restricted

Understanding these modes helps you anticipate when your app might be restricted.

2. Checking Battery Optimization Status

You can programmatically check if your app is battery optimized using Flutter’s platform channels.


import 'package:flutter/services.dart';

class BatteryOptimization {
  static const platform = MethodChannel('com.example.app/battery_optimization');

  Future<bool> isBatteryOptimizationEnabled() async {
    try {
      final bool enabled = await platform.invokeMethod('isBatteryOptimizationEnabled');
      return enabled;
    } on PlatformException catch (e) {
      print("Failed to check: '${e.message}'.");
      return true; // Assume enabled to err on the side of caution
    }
  }
}

And the corresponding Android implementation (MainActivity.kt):


import android.content.Context
import android.os.Build
import android.os.PowerManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/battery_optimization"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            if (call.method == "isBatteryOptimizationEnabled") {
                result.success(isBatteryOptimizationEnabled())
            } else {
                result.notImplemented()
            }
        }
    }

    private fun isBatteryOptimizationEnabled(): Boolean {
        val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            !powerManager.isIgnoringBatteryOptimizations(packageName)
        } else {
            false // Battery optimization doesn't exist before API 23
        }
    }
}

Make sure to add the required permissions in your AndroidManifest.xml:


<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.BATTERY_STATS"/>

3. Requesting Users to Disable Battery Optimization

If battery optimization is enabled, prompt the user to disable it for your app. Use the following Flutter code to call the native Android code:


import 'package:flutter/material.dart';

class BatteryOptimizationDialog extends StatelessWidget {
  final VoidCallback onDismiss;

  BatteryOptimizationDialog({required this.onDismiss});

  Future requestDisableBatteryOptimization() async {
    try {
      await BatteryOptimization.platform.invokeMethod('requestDisableBatteryOptimization');
    } on PlatformException catch (e) {
      print("Failed to request: '${e.message}'.");
    }
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Disable Battery Optimization?'),
      content: Text(
          'To ensure proper functionality, please disable battery optimization for this app.'),
      actions: <Widget>[
        TextButton(
          onPressed: onDismiss,
          child: Text('Cancel'),
        ),
        TextButton(
          onPressed: () {
            requestDisableBatteryOptimization();
            onDismiss();
          },
          child: Text('Disable'),
        ),
      ],
    );
  }
}

Corresponding Android implementation:


import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/battery_optimization"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            if (call.method == "isBatteryOptimizationEnabled") {
                result.success(isBatteryOptimizationEnabled())
            } else if (call.method == "requestDisableBatteryOptimization") {
                requestDisableBatteryOptimization()
                result.success(null)
            } else {
                result.notImplemented()
            }
        }
    }

    private fun isBatteryOptimizationEnabled(): Boolean {
        val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            !powerManager.isIgnoringBatteryOptimizations(packageName)
        } else {
            false // Battery optimization doesn't exist before API 23
        }
    }

    private fun requestDisableBatteryOptimization() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
                data = Uri.parse("package:$packageName")
            }
            startActivity(intent)
        }
    }
}

4. Using Foreground Services

Foreground services notify the user that the app is running a background task. Android is less likely to kill foreground services due to battery optimization.


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

class ForegroundService {
  static const platform = MethodChannel('com.example.app/foreground_service');

  Future startForegroundService() async {
    try {
      await platform.invokeMethod('startForegroundService');
    } on PlatformException catch (e) {
      print("Failed to start service: '${e.message}'.");
    }
  }

  Future stopForegroundService() async {
    try {
      await platform.invokeMethod('stopForegroundService');
    } on PlatformException catch (e) {
      print("Failed to stop service: '${e.message}'.");
    }
  }
}

Corresponding Android implementation:


import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.os.Bundle
import android.content.ContentResolver
import android.media.AudioAttributes
import android.net.Uri

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/foreground_service"
    private val SERVICE_CHANNEL_ID = "foreground_service_channel"
    private val SERVICE_NOTIFICATION_ID = 123

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            if (call.method == "startForegroundService") {
                startForegroundService()
                result.success(null)
            } else if (call.method == "stopForegroundService") {
                stopForegroundService()
                result.success(null)
            } else {
                result.notImplemented()
            }
        }
    }

    private fun startForegroundService() {
        val input = "Flutter Foreground Service"
        createNotificationChannel()

        val notificationIntent = Intent(this, MainActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(
            this,
            0,
            notificationIntent,
            PendingIntent.FLAG_IMMUTABLE
        )

        val notification = NotificationCompat.Builder(this, SERVICE_CHANNEL_ID)
            .setContentTitle("Foreground Service")
            .setContentText(input)
            .setSmallIcon(R.drawable.ic_notification)  // Replace with your notification icon
            .setContentIntent(pendingIntent)
            .build()

        startForegroundService(Intent(this, ForegroundService::class.java).apply {
            putExtra("inputExtra", input)
        })

        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(SERVICE_NOTIFICATION_ID, notification)
    }

    private fun stopForegroundService() {
        val serviceIntent = Intent(this, ForegroundService::class.java)
        stopService(serviceIntent)
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                SERVICE_CHANNEL_ID,
                "Foreground Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(serviceChannel)
        }
    }
}

Create the foreground service class (ForegroundService.kt):


import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import android.app.PendingIntent
import android.content.ContentResolver
import android.media.AudioAttributes
import android.net.Uri

class ForegroundService : Service() {

    private val SERVICE_CHANNEL_ID = "foreground_service_channel"
    private val SERVICE_NOTIFICATION_ID = 123

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val input = intent?.getStringExtra("inputExtra") ?: "Foreground Service"
        createNotificationChannel()

        val notificationIntent = Intent(this, MainActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(
            this,
            0,
            notificationIntent,
            PendingIntent.FLAG_IMMUTABLE
        )

        val notification = NotificationCompat.Builder(this, SERVICE_CHANNEL_ID)
            .setContentTitle("Foreground Service")
            .setContentText(input)
            .setSmallIcon(R.drawable.ic_notification) // Replace with your notification icon
            .setContentIntent(pendingIntent)
            .build()

        startForeground(SERVICE_NOTIFICATION_ID, notification)
        // Perform your long running task here
        return START_NOT_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                SERVICE_CHANNEL_ID,
                "Foreground Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            )

            val manager = getSystemService(NotificationManager::class.java)
            manager?.createNotificationChannel(serviceChannel)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
    }
}

Remember to add the service to your AndroidManifest.xml:


<service
    android:name=".ForegroundService"
    android:foregroundServiceType="dataSync|location"
    android:enabled="true"
    android:exported="false">
</service>

Add the foregroundServiceType based on the actual functionality your service provides. The valid types include:

  • dataSync
  • mediaPlayback
  • phoneCall
  • location
  • connectedDevice
  • camera
  • microphone

5. Using WorkManager for Deferrable Background Tasks

For tasks that don’t need to run immediately, use WorkManager. WorkManager is part of Android Jetpack and is designed to handle deferrable, guaranteed, and constraint-aware background tasks.


import 'package:workmanager/workmanager.dart';

void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) {
    switch (task) {
      case "simpleTask":
        print("Simple task triggered");
        break;
    }
    return Future.value(true);
  });
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: true,
  );
  Workmanager().registerOneOffTask("simpleTask", "simpleTask");
  runApp(MyApp());
}

This registers a simple one-off task using WorkManager.

First, add Workmanager to your pubspec.yaml:


dependencies:
  workmanager: ^0.5.0

Then in your `main.dart` or any other Dart file:


import 'dart:async';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: true,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text("Workmanager Example"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  Workmanager().registerOneOffTask(
                    "simpleTask",
                    "simpleTask",
                    initialDelay: const Duration(seconds: 5),
                  );
                },
                child: const Text("Register OneOff Task"),
              ),
              ElevatedButton(
                onPressed: () {
                  Workmanager().registerPeriodicTask(
                    "periodicTask",
                    "periodicTask",
                    initialDelay: const Duration(seconds: 5),
                  );
                },
                child: const Text("Register Periodic Task"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    switch (task) {
      case "simpleTask":
        debugPrint("Executing simple task");
        break;
      case "periodicTask":
        debugPrint("Executing periodic task");
        break;
    }
    return Future.value(true);
  });
}

6. Handling Wake Locks Carefully

Wake locks ensure the device stays awake, but they consume significant battery. Use them sparingly and release them as soon as the task is complete.


import 'package:wakelock/wakelock.dart';

Future acquireWakeLock() async {
  Wakelock.enable(); // Acquire wake lock
  // Perform tasks
  await Future.delayed(Duration(seconds: 10));
  Wakelock.disable(); // Release wake lock
}

7. Using AlarmManager for Scheduled Tasks (Less Recommended)

AlarmManager can schedule tasks at specific times but is less reliable with Doze mode. Consider WorkManager as a better alternative for most scheduling needs.


// Not recommended due to battery optimizations

8. Requesting Exemptions for Critical Tasks

In some critical cases, you may request an exemption from battery optimizations. This should be used sparingly and only when necessary.


//Check if battery optimizations are enabled

final bool _isBatteryOptimizationDisabled = await BatteryOptimization().isBatteryOptimizationEnabled();

if(_isBatteryOptimizationDisabled) {
    BatteryOptimization().requestDisableBatteryOptimization();
}

9. Testing on Real Devices

Emulators and simulators don’t always replicate real-world conditions. Always test your app on physical devices with different Android versions to ensure proper behavior under various battery optimization scenarios.

Best Practices

  • Minimize Background Tasks: Reduce the number and frequency of background tasks.
  • Batch Operations: Combine multiple operations into a single task to minimize overhead.
  • Use ConnectivityManager: Check network connectivity before performing network-related tasks.
  • Respect User Settings: Allow users to control background task behavior through app settings.

Conclusion

Handling battery optimization for background tasks in Flutter requires a combination of understanding OS behavior, using the appropriate APIs, and following best practices. By checking battery optimization status, requesting exemptions when necessary, using foreground services, leveraging WorkManager, and testing on real devices, you can create Flutter apps that are both functional and battery-efficient.