Using the workmanager Package to Execute Background Tasks Even When the App is Closed in Flutter

In Flutter development, executing background tasks is crucial for performing operations like data synchronization, sending notifications, or any process that shouldn’t block the main UI thread. The workmanager package is a powerful tool that enables you to run background tasks reliably, even when the app is closed or the device is restarted. This blog post delves into how to use the workmanager package effectively in Flutter.

What is the workmanager Package?

The workmanager package is a Flutter plugin that provides a unified interface for scheduling background tasks that are guaranteed to execute, regardless of the app’s state. It abstracts away the platform-specific details of background task scheduling (like WorkManager on Android and BGTaskScheduler on iOS), offering a consistent API for both platforms.

Why Use workmanager?

  • Reliability: Ensures tasks are executed even if the app is closed or the device is rebooted.
  • Flexibility: Supports one-off and periodic tasks.
  • Platform Abstraction: Simplifies background task management across Android and iOS.
  • Battery Efficiency: Respects system constraints to minimize battery drain.

How to Use the workmanager Package in Flutter

To implement background tasks using the workmanager package, follow these steps:

Step 1: Add Dependency

First, add the workmanager package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  workmanager: ^0.5.1

Then, run flutter pub get to install the package.

Step 2: Configure Native Platforms

The workmanager package requires some platform-specific configurations. Here’s how to configure it for Android and iOS:

Android Configuration
  1. Update AndroidManifest.xml:

    Add the necessary permissions and configuration to your android/app/src/main/AndroidManifest.xml file:

    
        <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
        <uses-permission android:name="android.permission.WAKE_LOCK"/>
        <application
            android:name="androidx.multidex.MultiDexApplication"
            android:label="your_app_name"
            android:icon="@mipmap/ic_launcher">
            <!-- Add this receiver -->
            <receiver
                android:name="dev.fluttercommunity.workmanager.FlutterWorkerBootReceiver"
                android:enabled="false"
                android:exported="true">
                <intent-filter>
                    <action android:name="android.intent.action.BOOT_COMPLETED"/>
                    <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
                </intent-filter>
            </receiver>
            <!-- Add this service -->
            <service
                android:name="dev.fluttercommunity.workmanager.FlutterWorkerService"
                android:exported="false"
                android:permission="android.permission.BIND_JOB_SERVICE" />
            <activity
                android:name=".MainActivity"
                android:launchMode="singleTop"
                android:theme="@style/LaunchTheme"
                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
                android:hardwareAccelerated="true"
                android:windowSoftInputMode="adjustResize">
                <!-- Specifies an Android theme to apply to this Activity as soon as
                     the Android process has started. This theme is visible to the user
                     while the Flutter UI initializes. After that, this theme continues
                     to determine the Window background behind the Flutter UI. -->
                <meta-data
                  android:name="io.flutter.embedding.android.NormalTheme"
                  android:resource="@style/NormalTheme"
                  />
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>
                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
            <!-- Don't delete the meta-data below.
                 This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
            <meta-data
                android:name="flutterEmbedding"
                android:value="2" />
        </application>
    
    
  2. Enable MultiDex:

    If your app uses multidex, ensure it’s properly configured. If you encounter issues, extend MultiDexApplication:

    
    // android/app/src/main/kotlin/YourApplication.kt
    package your.package.name
    import io.flutter.app.FlutterApplication
    import io.flutter.plugin.common.PluginRegistry
    import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback
    import androidx.multidex.MultiDex
    import android.content.Context
    class Application : FlutterApplication(), PluginRegistrantCallback {
        override fun attachBaseContext(base: Context) {
            super.attachBaseContext(base)
            MultiDex.install(this)
        }
        override fun registerWith(registry: PluginRegistry) {
            //GeneratedPluginRegistrant.registerWith(registry);
        }
    }
    

    And update your AndroidManifest.xml:

    <application
        android:name=".Application"
        android:label="your_app_name"
        android:icon="@mipmap/ic_launcher">
    
iOS Configuration
  1. Background Modes:

    Enable Background Modes in your Xcode project (Runner.xcworkspace) under the Signing & Capabilities tab. Add the “Background processing” mode.

Step 3: Initialize Workmanager

Initialize Workmanager in your Flutter app, typically in the main() function, before running the app:

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

void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) {
    switch (task) {
      case 'simpleTask':
        print("Simple task is running");
        break;
      case Workmanager.iOSBackgroundTask:
        print("iOS background task is running");
        break;
    }
    return Future.value(true);
  });
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: true,
  );
  
  runApp(MyApp());
}
  • callbackDispatcher: This function is the entry point for background tasks. It must be a top-level function (not a method inside a class).
  • Workmanager().initialize: Initializes the Workmanager plugin.
  • isInDebugMode: Set to true during development for detailed logging.

Step 4: Register a Background Task

Register a background task using Workmanager().registerOneOffTask or Workmanager().registerPeriodicTask:

Workmanager().registerOneOffTask(
  "task-identifier",
  "simpleTask",
  initialDelay: Duration(seconds: 5),
  inputData: {'key': 'value'},
  constraints: Constraints(
    requiresCharging: false,
    networkType: NetworkType.connected,
  ),
);

Workmanager().registerPeriodicTask(
  "periodic-task-identifier",
  "simpleTask",
  initialDelay: Duration(minutes: 15),
  constraints: Constraints(
    requiresCharging: true,
  ),
  existingWorkPolicy: ExistingWorkPolicy.replace,
);
  • registerOneOffTask: Schedules a task to run once.
  • registerPeriodicTask: Schedules a task to run periodically.
  • task-identifier: A unique identifier for the task.
  • simpleTask: The name of the task to execute. This must match a case in your callbackDispatcher.
  • initialDelay: The time to wait before running the task.
  • inputData: Optional data that can be passed to the task.
  • Constraints: Conditions that must be met for the task to run (e.g., network connectivity, charging).
  • ExistingWorkPolicy: Determines what to do if a task with the same ID already exists.

Step 5: Handle Background Task Logic

Implement the logic for your background task in the callbackDispatcher function:

void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    switch (task) {
      case 'simpleTask':
        print("Simple task is running");
        // Perform your background task logic here
        break;
      case Workmanager.iOSBackgroundTask:
        print("iOS background task is running");
        // Perform iOS-specific background logic
        break;
    }
    return Future.value(true);
  });
}
  • The executeTask method provides the task identifier and inputData.
  • The switch statement allows you to handle different tasks.
  • Ensure to return a Future.value(true) to indicate that the task has completed successfully.

Best Practices for Using workmanager

  • Keep Tasks Short: Minimize the execution time of your background tasks to conserve battery.
  • Handle Errors: Implement error handling to gracefully manage exceptions and prevent crashes.
  • Use Constraints Wisely: Leverage constraints like requiresCharging and networkType to optimize task scheduling.
  • Test Thoroughly: Test background tasks under various conditions, including app closure and device reboot, to ensure reliability.

Conclusion

The workmanager package provides a reliable and efficient way to execute background tasks in Flutter, even when the app is closed. By following the steps outlined in this guide, you can seamlessly integrate background processing into your Flutter applications, ensuring essential tasks are completed reliably and efficiently. Proper configuration and thoughtful implementation are key to leveraging the full potential of workmanager.