In Flutter development, background tasks are essential for executing operations that shouldn’t block the main thread, such as data synchronization, network requests, or complex computations. Effectively managing the different states of these background tasks is crucial for ensuring a smooth user experience and reliable application behavior. This blog post delves into how to handle different background task states in Flutter with best practices and code examples.
Understanding Background Task States
Before diving into implementation, it’s important to understand the common states a background task can be in:
- Idle: The task is waiting to start.
- Running: The task is currently executing.
- Completed: The task has finished successfully.
- Failed: The task has encountered an error and could not complete.
- Cancelled: The task was manually cancelled before completion.
Basic Implementation
Let’s start with a basic example of performing a background task using async
and await
:
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: TaskScreen(),
);
}
}
class TaskScreen extends StatefulWidget {
@override
_TaskScreenState createState() => _TaskScreenState();
}
class _TaskScreenState extends State {
String taskState = 'Idle';
String taskResult = '';
Future performBackgroundTask() async {
setState(() {
taskState = 'Running';
});
try {
// Simulate a long-running task
await Future.delayed(Duration(seconds: 3));
setState(() {
taskState = 'Completed';
taskResult = 'Task completed successfully!';
});
} catch (e) {
setState(() {
taskState = 'Failed';
taskResult = 'Task failed with error: ${e.toString()}';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Background Task Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Task State: $taskState'),
SizedBox(height: 20),
Text('Task Result: $taskResult'),
SizedBox(height: 20),
ElevatedButton(
onPressed: performBackgroundTask,
child: Text('Start Task'),
),
],
),
),
);
}
}
This example updates the UI based on the task’s state changes (Idle, Running, Completed, Failed) and displays a corresponding result.
Advanced State Management with Streams
For more complex scenarios, using streams can provide a reactive and efficient way to manage background task states.
Implementing Task State Management with Streams
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
enum TaskState { idle, running, completed, failed }
class TaskManager {
final _taskStateController = StreamController();
final _taskResultController = StreamController();
Stream get taskStateStream => _taskStateController.stream;
Stream get taskResultStream => _taskResultController.stream;
Future performBackgroundTask() async {
_taskStateController.add(TaskState.running);
try {
// Simulate a long-running task
await Future.delayed(Duration(seconds: 3));
_taskStateController.add(TaskState.completed);
_taskResultController.add('Task completed successfully!');
} catch (e) {
_taskStateController.add(TaskState.failed);
_taskResultController.add('Task failed with error: ${e.toString()}');
}
}
void dispose() {
_taskStateController.close();
_taskResultController.close();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: TaskScreen(),
);
}
}
class TaskScreen extends StatefulWidget {
@override
_TaskScreenState createState() => _TaskScreenState();
}
class _TaskScreenState extends State {
final taskManager = TaskManager();
@override
void dispose() {
taskManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Background Task Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
StreamBuilder<TaskState>(
stream: taskManager.taskStateStream,
initialData: TaskState.idle,
builder: (context, snapshot) {
return Text('Task State: ${snapshot.data}');
},
),
SizedBox(height: 20),
StreamBuilder<String>(
stream: taskManager.taskResultStream,
initialData: '',
builder: (context, snapshot) {
return Text('Task Result: ${snapshot.data}');
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
taskManager.performBackgroundTask();
},
child: Text('Start Task'),
),
],
),
),
);
}
}
Key improvements in this approach:
- Centralized Task Management: The
TaskManager
class encapsulates the task logic and state management. - Reactive Updates: The UI updates reactively based on stream emissions.
- Clear State Enumeration: The
TaskState
enum makes state handling more explicit and maintainable. - StreamControllers: Provides stream controllers to manage states and result and provides stream listeners for different states in the UI.
Using Flutter Background Services
For tasks that need to run even when the app is not in the foreground, Flutter’s background services or isolates can be used.
Example using flutter_background_service
package
Step 1: Add the flutter_background_service
package
dependencies:
flutter_background_service: ^4.0.0
Step 2: Configure 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<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeService();
runApp(MyApp());
}
Future initializeService() async {
final service = FlutterBackgroundService();
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
autoStart: true,
isForegroundMode: true,
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
onBackground: onIosBackground,
),
);
service.startService();
}
@pragma('vm:entry-point')
Future<bool> onIosBackground(ServiceInstance service) async {
WidgetsFlutterBinding.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();
});
Timer.periodic(const Duration(seconds: 1), (timer) async {
if (!(service is AndroidServiceInstance)) return;
if (await service.isForegroundService()) {
service.setForegroundNotificationInfo(
title: "Flutter Background Service",
content: "Updated at ${DateTime.now()}",
);
}
/// you can see this log in the background.
print('FLUTTER BACKGROUND SERVICE: ${DateTime.now()}');
});
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Background Service Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
FlutterBackgroundService().invoke("setAsForeground");
},
child: Text("Set to Foreground"),
),
ElevatedButton(
onPressed: () {
FlutterBackgroundService().invoke("setAsBackground");
},
child: Text("Set to Background"),
),
ElevatedButton(
onPressed: () {
FlutterBackgroundService().invoke("stopService");
},
child: Text("Stop Service"),
),
],
),
),
);
}
}
This example demonstrates:
- Setting up a background service with foreground notification support.
- Defining handlers to switch between foreground and background modes.
- Periodically updating the foreground notification to show it’s running.
Cancellation and Error Handling
Background tasks can be cancelled or may encounter errors. Handle these scenarios gracefully:
Cancelling a Task
import 'dart:async';
import 'package:flutter/material.dart';
class TaskScreen extends StatefulWidget {
@override
_TaskScreenState createState() => _TaskScreenState();
}
class _TaskScreenState extends State {
String taskState = 'Idle';
String taskResult = '';
StreamSubscription? taskSubscription;
Future performBackgroundTask() async {
setState(() {
taskState = 'Running';
});
try {
taskSubscription = Stream.periodic(Duration(seconds: 1), (count) => count)
.take(5) // Simulate a task that runs for 5 seconds
.listen((count) {
print('Running: $count');
setState(() {
taskResult = 'Running... $count';
});
}, onDone: () {
setState(() {
taskState = 'Completed';
taskResult = 'Task completed successfully!';
});
}, onError: (error) {
setState(() {
taskState = 'Failed';
taskResult = 'Task failed with error: ${error.toString()}';
});
});
} catch (e) {
setState(() {
taskState = 'Failed';
taskResult = 'Task failed with error: ${e.toString()}';
});
}
}
void cancelTask() {
taskSubscription?.cancel();
setState(() {
taskState = 'Cancelled';
taskResult = 'Task cancelled.';
});
}
@override
void dispose() {
taskSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Background Task Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Task State: $taskState'),
SizedBox(height: 20),
Text('Task Result: $taskResult'),
SizedBox(height: 20),
ElevatedButton(
onPressed: taskState == 'Idle' || taskState == 'Cancelled'
? performBackgroundTask
: null,
child: Text('Start Task'),
),
ElevatedButton(
onPressed: taskState == 'Running' ? cancelTask : null,
child: Text('Cancel Task'),
),
],
),
),
);
}
}
Error Handling
Handle exceptions inside your background tasks using try-catch
blocks and update the UI accordingly.
Future performBackgroundTask() async {
setState(() {
taskState = 'Running';
});
try {
// Simulate a long-running task
await Future.delayed(Duration(seconds: 3));
// Simulate an error
throw Exception('Simulated error!');
setState(() {
taskState = 'Completed';
taskResult = 'Task completed successfully!';
});
} catch (e) {
setState(() {
taskState = 'Failed';
taskResult = 'Task failed with error: ${e.toString()}';
});
}
}
Conclusion
Properly handling background task states in Flutter is crucial for providing a reliable and smooth user experience. Whether using basic async/await
, reactive streams, or background services, understanding the lifecycle and potential states of your tasks will lead to more robust and maintainable applications. Remember to handle errors, cancellations, and keep the user informed about the progress and status of background operations.