Handling Different Background Task States in Flutter

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: &ltWidget>[
            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: &ltWidget>[
            StreamBuilder&ltTaskState>(
              stream: taskManager.taskStateStream,
              initialData: TaskState.idle,
              builder: (context, snapshot) {
                return Text('Task State: ${snapshot.data}');
              },
            ),
            SizedBox(height: 20),
            StreamBuilder&ltString>(
              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&ltvoid> 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&ltbool> 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: &ltWidget>[
            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: &ltWidget>[
            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.