Implementing Background Tasks and Services in Flutter Mobile Apps

In modern mobile application development, executing tasks independently of the user interface (UI) is crucial for maintaining responsiveness and enabling various functionalities. This is where background tasks and services come into play. Flutter, Google’s UI toolkit, offers robust support for implementing background tasks and services. This comprehensive guide will delve into how to effectively implement background tasks and services in Flutter applications.

What are Background Tasks and Services?

Background tasks and services are processes that run in the background without requiring active user interaction. These tasks can handle operations such as data synchronization, push notifications, location updates, and periodic data processing, enhancing the overall user experience and app efficiency.

Why Implement Background Tasks and Services?

  • Enhanced User Experience: Offload long-running tasks from the main thread, ensuring the UI remains responsive.
  • Real-Time Updates: Enable functionalities like push notifications and real-time data synchronization.
  • Efficient Data Processing: Perform periodic data processing or maintenance tasks without interrupting the user.
  • Location-Based Services: Track user location in the background for location-aware applications.

Approaches to Implementing Background Tasks in Flutter

Flutter provides several methods to implement background tasks, each with its strengths and use cases:

1. Using compute Function

The compute function allows you to run computationally intensive tasks in a separate isolate, preventing the UI thread from blocking.

Step 1: Define a Function for Background Task

import 'package:flutter/foundation.dart';

Future<int> intensiveComputation(int input) async {
  // Simulate a computationally intensive task
  await Future.delayed(Duration(seconds: 2));
  return input * 2;
}
Step 2: Use the compute Function in Flutter

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Compute Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int result = 0;

  Future<void> performComputation() async {
    final data = 10;
    final computedResult = await compute(intensiveComputation, data);
    setState(() {
      result = computedResult;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Compute Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Result: $result',
              style: TextStyle(fontSize: 24),
            ),
            ElevatedButton(
              onPressed: performComputation,
              child: Text('Compute'),
            ),
          ],
        ),
      ),
    );
  }
}

Future<int> intensiveComputation(int input) async {
  // Simulate a computationally intensive task
  await Future.delayed(Duration(seconds: 2));
  return input * 2;
}

In this example:

  • The intensiveComputation function simulates a long-running task.
  • The compute function runs intensiveComputation in a separate isolate.
  • The UI remains responsive while the computation occurs.

2. Using Flutter Services with flutter_background_service Package

The flutter_background_service package simplifies the implementation of background services, allowing tasks to run even when the app is minimized or closed.

Step 1: Add Dependency

Add the flutter_background_service package to your pubspec.yaml:


dependencies:
  flutter_background_service: ^4.0.0
Step 2: Initialize and Configure the 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';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeService();
  runApp(MyApp());
}

Future<void> initializeService() async {
  final service = FlutterBackgroundService();
  await service.configure(
    androidConfiguration: AndroidConfiguration(
      // this will be executed when app is in foreground or background in separated isolate
      onStart: onStart,

      // auto start service
      autoStart: true,
      isForegroundMode: true,
    ),
    iosConfiguration: IosConfiguration(
      // auto start service
      autoStart: true,

      // this will be executed when app is in foreground in separated isolate
      onForeground: onStart,

      // you have to enable background fetch capability on xcode project
      onBackground: null,
    ),
  );
}

@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 every 1 seconds
  Timer.periodic(const Duration(seconds: 1), (timer) async {
    if (service is AndroidServiceInstance) {
      if (await service.isForegroundService()) {
        service.setForegroundService();
      }
    }

    print('Background service running: ${DateTime.now()}');
    /// you can see this log on the device log
    /// logcat -f /sdcard/output.txt
  });
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Service app'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                child: const Text("Foreground Mode"),
                onPressed: () {
                  FlutterBackgroundService().invoke('setAsForeground');
                },
              ),
              ElevatedButton(
                child: const Text("Background Mode"),
                onPressed: () {
                  FlutterBackgroundService().invoke('setAsBackground');
                },
              ),
              ElevatedButton(
                child: const Text("Stop Service"),
                onPressed: () {
                  FlutterBackgroundService().invoke('stopService');
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

In this configuration:

  • The initializeService function configures the background service.
  • The onStart function is executed in a separate isolate and contains the background task logic.
  • The service runs every 1 second, printing a log message.
  • The DartPluginRegistrant.ensureInitialized() ensures Flutter plugins are properly initialized in the background isolate.
Step 3: Android Manifest Configuration

Add the necessary permissions and service declaration to your AndroidManifest.xml:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.your_app">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="your_app"
        android:icon="@mipmap/ic_launcher">

        <service
            android:name="com.example.your_app.BackgroundService"
            android:enabled="true"
            android:exported="false"
            android:permission="android.permission.BIND_JOB_SERVICE" />

        <receiver android:name="com.example.your_app.BootReceiver"
            android:enabled="true"
            android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

    </application>
</manifest>

3. Using WorkManager

The WorkManager API is part of Android Jetpack and is suitable for deferrable, guaranteed, and constraint-aware background tasks.

Step 1: Add Dependency

Include the WorkManager dependency in your build.gradle file:


dependencies {
    implementation("androidx.work:work-runtime-ktx:2.9.0")
}
Step 2: Define a Worker Class

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlinx.coroutines.delay

class MyWorker(context: Context, workerParams: WorkerParameters) :
    CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        // Perform background task here
        delay(2000) // Simulate a background task
        println("WorkManager is running: ${DateTime.now()}")
        return Result.success()
    }
}
Step 3: Enqueue the Work Request in Flutter

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(
    "periodicTask",
    "simplePeriodicTask",
    initialDelay: Duration(seconds: 5),
    frequency: Duration(minutes: 15),
  );

  runApp(MyApp());
}

void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) {
    switch (task) {
      case "simplePeriodicTask":
        print("Background task running: ${DateTime.now()}");
        break;
    }
    return Future.value(true);
  });
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('WorkManager Demo'),
        ),
        body: Center(
          child: Text('WorkManager Example'),
        ),
      ),
    );
  }
}

In this setup:

  • A CoroutineWorker is defined to perform the background task.
  • The Workmanager().initialize() configures the WorkManager.
  • The Workmanager().registerPeriodicTask() registers the periodic task to run every 15 minutes.

4. Using Isolate Package for CPU Intensive Tasks

The isolate package is ideal for running Dart code concurrently on separate threads. It’s useful for CPU-intensive tasks that can cause UI jank if run on the main thread.

Step 1: Import Isolate Package

First, ensure you have the isolate package in your dependencies.


dependencies:
  flutter:
    sdk: flutter
  isolate: ^2.2.1  # Add the isolate package here
Step 2: Create a Separate Isolate for the Background Task

Use the Isolate.run method to run the intensive task in a separate isolate. Here is how:


import 'dart:isolate';

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _result = 'Performing calculation...';

  @override
  void initState() {
    super.initState();
    performBackgroundTask();
  }

  void performBackgroundTask() async {
    final receivePort = ReceivePort();
    Isolate.spawn(
      heavyCalculation,
      receivePort.sendPort,
    );

    receivePort.listen((message) {
      setState(() {
        _result = message.toString();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Isolate Example'),
      ),
      body: Center(
        child: Text(_result, style: const TextStyle(fontSize: 20)),
      ),
    );
  }
}

Future<void> heavyCalculation(SendPort sendPort) async {
  // Simulate a computationally heavy task
  int result = 0;
  for (int i = 0; i < 1000000000; i++) {
    result += i;
  }

  sendPort.send('Result: $result');
}

Here is how it works:

  • First create Heavy Calculation method and create main application file.
  • Use StatefulWidget lifecycle, inside that create background task and wait for Result to print the actual calculated result from other thread to your app thread.
  • Make sure you have implemented receive port concept with sender port in isolated class, so the message transfer with isolated thread with StateFulWidget class instance, after complete the all necessary steps, you have see your Result in your App main screen.

Best Practices for Background Tasks in Flutter

  • Minimize Battery Usage:
    • Use background tasks sparingly and only when necessary.
    • Batch tasks together to reduce the frequency of background execution.
    • Use appropriate constraints to run tasks when the device is charging or connected to Wi-Fi.
  • Handle Permissions Carefully:
    • Request necessary permissions (e.g., location, background execution) at runtime.
    • Provide clear explanations for why the permissions are required.
    • Handle permission denial gracefully.
  • Ensure Reliability:
    • Implement error handling and retry mechanisms for background tasks.
    • Use persistent storage to save state and ensure tasks can resume after app restarts.
    • Monitor task execution and log any errors or failures.
  • Use Heads-Up Notifications:
    • Avoid high-priority and intrusive heads-up notifications.
    • If it needs to display notifications you should display on crucial steps.
      Avoid displaying the heads-up-notification with some calculation background tasks, end of calculations you can show some heads-up-notifications and ask some questions related result for user interaction.
  • Isolate Heavy Computation:
    • CPU Intensive operations perform separately, do not lock or freezing your main-ui display operation.
    • Always transfer calculated result data from CPU intensive calculation isolated operation and display the data to your UI based view.
    • By doing so, make sure user is seeing real result and operation does’nt hold you view.

Conclusion

Implementing background tasks and services in Flutter apps is essential for delivering a responsive and efficient user experience. By using techniques like the compute function, flutter_background_service package, WorkManager, and isolate concurrency, developers can perform various tasks in the background without impacting UI performance. Adhering to best practices ensures that background tasks are battery-efficient, reliable, and user-friendly.