In Flutter development, managing background tasks effectively is crucial for creating robust and user-friendly applications. Background tasks allow you to perform operations like data synchronization, push notifications, or heavy computations without blocking the main UI thread. However, these tasks have different states and are influenced by lifecycle events. This blog post explores how to handle different states and lifecycle events of background tasks in Flutter.
Why Handle Background Task States and Lifecycle Events?
- Resource Management: Properly managing task states helps in efficient resource utilization, preventing memory leaks or unnecessary CPU usage.
- Data Consistency: Ensuring data consistency is vital when background tasks are involved in data modification.
- User Experience: Informing users about the status of background tasks (e.g., through notifications) enhances the overall user experience.
- Avoiding Errors: Lifecycle awareness helps in preventing common issues such as performing UI updates when the UI is no longer active.
Key Lifecycle Events to Consider
- App Launch: Initializing or resuming tasks when the app starts.
- App Resumed: Restarting or continuing tasks when the app comes back to the foreground.
- App Paused: Pausing or stopping tasks when the app goes to the background.
- App Suspended/Terminated: Cleaning up and persisting the state of tasks before the app is terminated.
Methods for Handling Background Tasks in Flutter
1. Using async
and await
The simplest way to run code in the background is by using async
and await
with Dart’s Future
. However, this approach is suitable for short, non-critical tasks that don’t need to survive app termination.
import 'dart:async';
void main() async {
print('Starting background task...');
await performBackgroundTask();
print('Background task completed.');
}
Future performBackgroundTask() async {
await Future.delayed(Duration(seconds: 5));
print('Task executed in the background.');
}
This method is straightforward, but it’s not suitable for long-running tasks that need to survive the app’s lifecycle.
2. Flutter Background Services
For more complex scenarios, Flutter provides plugins and methods to run background services, such as using the flutter_background_service
package.
Step 1: Add the flutter_background_service
Dependency
dependencies:
flutter_background_service: ^2.0.0
Step 2: Implement the Background Service
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeService();
runApp(MyApp());
}
Future initializeService() async {
final service = FlutterBackgroundService();
await service.configure(
androidConfiguration: AndroidConfiguration(
// this will executed when app is in foreground inseparated isolate
onStart: onStart,
// auto start service
autoStart: true,
isForegroundMode: true,
),
iosConfiguration: IosConfiguration(
// auto start service
autoStart: true,
// this will executed when app is in foreground inseparated isolate
onForeground: onStart,
// you have to enable background fetch capability on xcode project
onBackground: onIosBackground,
),
);
service.startService();
}
// to ensure this is executed
// run app from xcode, then from the xcode menu, select Debug > Attach to Process > Your App
// set chooseDifferentPackageName: true, set packageName: , then launch to debug the background isolate (https://github.com/fluttercommunity/flutter_foreground_task/issues/88)
@pragma('vm:entry-point')
Future onIosBackground(ServiceInstance service) async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
return true;
}
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
if (service is AndroidServiceInstance) {
service.on('setAsForeground').listen((event) {
service.setAsForegroundService();
});
service.on('setAsBackground').listen((event) {
service.setAsBackgroundService();
});
}
service.on('stopService').listen((event) {
service.stopSelf();
});
// simple task
Timer.periodic(const Duration(seconds: 1), (timer) async {
if (service is AndroidServiceInstance) {
if (await service.isForegroundService()) {
service.setForegroundServiceFirstNotification(
title: "My App Service",
content: "Updated at ${DateTime.now()}",
);
}
}
print('Background service is running at ${DateTime.now()}');
});
}
class MyApp extends StatefulWidget {
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
String text = "Stop Service";
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Service App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Service is running',
),
ElevatedButton(
onPressed: () {
final service = FlutterBackgroundService();
service.invoke("stopService");
Navigator.of(context).pop();
},
child: Text(
text,
),
),
],
),
),
),
);
}
}
Key points:
initializeService()
initializes and configures the background service.onStart()
defines the task that the background service will perform.FlutterBackgroundService()
is used to start, stop, and configure the background service.
3. WorkManager
WorkManager
is an API provided by Android Jetpack for managing background tasks. It is particularly useful for deferrable, guaranteed tasks that need to be executed even if the app is closed or the device restarts. For Flutter, you can use the workmanager
package.
Step 1: Add the workmanager
Dependency
dependencies:
workmanager: ^0.5.0
Step 2: Implement the WorkManager
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
Workmanager().initialize(
callbackDispatcher, // The top level function, aka callbackDispatcher
isInDebugMode: true, // If enabled it will post a notification whenever the task is running. Useful for debugging tasks
);
Workmanager().registerPeriodicTask(
"2",
"simplePeriodicTask",
frequency: Duration(minutes: 15),
);
runApp(MyApp());
}
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) {
switch (task) {
case Workmanager.BACKGROUND_TASK_DEFAULT_TAG:
// ignore: avoid_print
print(
"Native adapter background task is executing. task: $task inputData: $inputData");
break;
case "simpleTask":
// ignore: avoid_print
print("$task was executed. inputData = $inputData");
break;
case "simplePeriodicTask":
// ignore: avoid_print
print(
"$task was executed. inputData = $inputData");
break;
}
return Future.value(true);
});
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("Workmanager Example"),
),
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: () {
Workmanager().registerOneOffTask(
"task-identifier",
"simpleTask",
inputData: {'int': 1, 'bool': true, 'string': 'str'},
);
},
child: Text("Register OneOff Task")),
ElevatedButton(
onPressed: () {
Workmanager().cancelByUniqueName("task-identifier");
},
child: Text("Cancel OneOff Task"))
],
),
),
),
);
}
}
Key Components:
Workmanager().initialize
initializes the WorkManager with a callback dispatcher.callbackDispatcher
is a top-level function that handles different tasks.Workmanager().registerOneOffTask
registers a task to run once.Workmanager().registerPeriodicTask
registers a task to run periodically.
Handling Different States and Lifecycle Events
App Launch
When the app launches, you may need to resume any pending background tasks or initialize new ones.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeBackgroundTasks();
runApp(MyApp());
}
Future initializeBackgroundTasks() async {
// Initialize or resume background tasks
}
App Resumed
When the app is resumed from the background, you may want to check the status of any ongoing tasks and update the UI accordingly.
import 'package:flutter/widgets.dart';
import 'package:flutter/scheduler.dart';
class _MyAppState extends State with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// Handle resumed state
checkBackgroundTaskStatus();
}
}
void checkBackgroundTaskStatus() {
// Check and update UI
}
}
App Paused/Suspended
When the app is paused or suspended, it’s important to pause or stop any non-critical tasks to conserve resources.
import 'package:flutter/widgets.dart';
class _MyAppState extends State with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// Pause or stop tasks
pauseBackgroundTasks();
}
if (state == AppLifecycleState.detached) {
_disconnect(); //Remove services when app is closing.
}
if(state == AppLifecycleState.inactive){
_disconnect(); //When route is remove
}
}
void pauseBackgroundTasks() {
// Pause background tasks logic
}
void _disconnect(){
// logic when application goes out for all processings.
}
}
App Terminated
Before the app is terminated, you should clean up resources and persist the state of any ongoing tasks.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Persist task states
await persistTaskStates();
runApp(MyApp());
}
Future persistTaskStates() async {
// Logic to save the state of tasks
}
Best Practices
- Use Background Services for Long-Running Tasks: For tasks that need to run even when the app is in the background.
- Utilize WorkManager for Deferrable Tasks: For tasks that must be completed reliably, even if the device restarts.
- Handle App Lifecycle Events: Manage task states based on lifecycle events to optimize resource usage.
- Inform the User: Provide feedback to the user about the status of background tasks to improve the user experience.
- Test Thoroughly: Test background tasks in different scenarios to ensure they work as expected.
Conclusion
Handling different states and lifecycle events of background tasks in Flutter is essential for building efficient, reliable, and user-friendly applications. By using the appropriate tools and techniques, you can ensure that background tasks perform correctly under various conditions, enhancing the overall performance and user experience of your app. Understanding and implementing these practices is a key aspect of Flutter development.