Using Services and Plugins for Background Operations in Flutter

In Flutter development, running background tasks is crucial for various applications, such as data synchronization, push notifications, or performing resource-intensive operations without blocking the UI. This can be achieved through services and plugins, allowing Flutter apps to handle tasks even when the app is not in the foreground. This comprehensive guide will explore how to use services and plugins for background operations in Flutter.

Understanding Background Operations in Flutter

Background operations enable an app to perform tasks even when it’s not the active, foreground app. Flutter provides several mechanisms for implementing background operations, each with its own set of advantages and use cases.

Why Use Background Operations?

  • Improved User Experience: Users can continue using the app without interruptions.
  • Task Execution: Enables execution of long-running tasks without affecting UI performance.
  • Scheduled Tasks: Allows scheduling tasks at specific times, such as nightly data backups.
  • Real-time Updates: Facilitates real-time data synchronization and push notifications.

Methods for Background Operations in Flutter

Flutter provides several methods for implementing background operations, including:

  • Services: Running long-lived operations with native platform services.
  • Plugins: Leveraging native platform features and APIs.
  • WorkManager: A reliable way to schedule deferrable, guaranteed background work (Android only).
  • Flutter Isolate: Executing Dart code in a separate thread.

Implementing Services for Background Operations

Services are components that run in the background, performing long-running operations without any user interface. Services are typically used for tasks like playing music, handling network operations, or managing sensors.

Creating a Flutter Service

To create a service in Flutter, you’ll need to interact with the native platform using platform channels.

Step 1: Set Up the Flutter Project

Start by creating a new Flutter project or navigating to an existing one:

flutter create background_service_app
cd background_service_app
Step 2: Define the Platform Channel

Define a platform channel in your Flutter code to communicate with the native platform (Android/iOS):

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

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

class MyApp extends StatelessWidget {
  static const platform = const MethodChannel('background_service_channel');

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Background Service Example'),
        ),
        body: Center(
          child: ElevatedButton(
            child: Text('Start Background Service'),
            onPressed: () {
              _startBackgroundService();
            },
          ),
        ),
      ),
    );
  }

  Future _startBackgroundService() async {
    try {
      await platform.invokeMethod('startService');
    } on PlatformException catch (e) {
      print("Failed to start service: '${e.message}'.");
    }
  }
}
Step 3: Implement the Native Service (Android)

For Android, implement a native service in Java/Kotlin. Create a new class that extends Service:

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.MethodCall;
import androidx.annotation.Nullable;

public class BackgroundService extends Service implements MethodCallHandler {
    private static final String CHANNEL = "background_service_channel";
    private MethodChannel methodChannel;

    @Override
    public void onCreate() {
        super.onCreate();
        methodChannel = new MethodChannel(getFlutterEngine().getDartExecutor(), CHANNEL);
        methodChannel.setMethodCallHandler(this);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("BackgroundService", "Service started");
        startBackgroundOperation();
        return START_STICKY;
    }

    private void startBackgroundOperation() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        Log.d("BackgroundService", "Running background task");
                        Thread.sleep(5000); // Simulate background task
                    }
                } catch (InterruptedException e) {
                    Log.e("BackgroundService", "Background task interrupted");
                }
            }
        }).start();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("BackgroundService", "Service destroyed");
    }

    @Override
    public void onMethodCall(MethodCall call, Result result) {
        if (call.method.equals("startService")) {
            result.success("Service started");
        } else {
            result.notImplemented();
        }
    }
}
Step 4: Register the Service in AndroidManifest.xml

Register the service in your AndroidManifest.xml file:


    
        
        
        
            
            
        
    
    
    
Step 5: Implement the Native Service (iOS)

For iOS, implement the background service using Swift/Objective-C. Create a method in your AppDelegate.swift or AppDelegate.m:

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let backgroundChannel = FlutterMethodChannel(name: "background_service_channel",
                                              binaryMessenger: controller.binaryMessenger)
    backgroundChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "startService" {
        self.startBackgroundService(result: result)
      } else {
        result(FlutterMethodNotImplemented)
      }
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  func startBackgroundService(result: @escaping FlutterResult) {
    // Implement your background task here
    print("Starting background service in iOS")
    result("Service started")
  }
}

Ensure you configure background modes in your Xcode project:

  1. Open Runner.xcworkspace in Xcode.
  2. Go to Runner -> Signing & Capabilities.
  3. Add a new capability and select Background Modes.
  4. Check the Background fetch and Remote notifications options as needed.

Using Plugins for Background Operations

Flutter plugins are pre-built packages that provide access to platform-specific features and APIs. Several plugins are available to facilitate background operations.

Popular Plugins for Background Operations

  • flutter_background_service: Provides a way to run Flutter code in the background as a service.
  • workmanager: Schedules background tasks using Android’s WorkManager API.
  • android_alarm_manager_plus: Schedules alarms in the background using Android’s AlarmManager.

Example: Using the flutter_background_service Plugin

Step 1: Add the Dependency

Add the flutter_background_service plugin to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  flutter_background_service: ^4.0.0
Step 2: Implement the Background Service

Implement the background service using the plugin’s API:

import 'dart:async';
import 'dart:io';
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(const MyApp());
}

Future initializeService() async {
  final service = FlutterBackgroundService();

  await service.configure(
    androidConfiguration: AndroidConfiguration(
      // this will executed when app is in foreground in separated 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 in separated isolate
      onForeground: onStart,

      // to resume auto execute process after app close
      onBackground: onIosBackground,
    ),
  );

  service.startService();
}

// to ensure this is executed
// run app from xcode, then from xcode menu, select Simulate Background Fetch
bool onIosBackground(ServiceInstance service) {
  WidgetsFlutterBinding.ensureInitialized();
  print('FLUTTER BACKGROUND FETCH');

  return true;
}

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) {
  //     if (await service.isForegroundTask) {
  //       /// you can see this logcat if app is in foreground
  //       print("Foreground Service");
  //     }
  //   }

  //   /// you can see this logcat if app is in background
  //   print('Background Service ${DateTime.now()}');
  // });
}

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

  @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('Flutter Background Service'),
        ),
        body: Column(
          children: [
            ElevatedButton(
              child: const Text("Foreground Mode"),
              onPressed: () {
                FlutterBackgroundService().invoke("setAsForeground");
              },
            ),
            ElevatedButton(
              child: const Text("Background Mode"),
              onPressed: () {
                FlutterBackgroundService().invoke("setAsBackground");
              },
            ),
            ElevatedButton(
              child: Text(text),
              onPressed: () async {
                final service = FlutterBackgroundService();
                var isRunning = await service.isRunning();
                if (isRunning) {
                  service.stopService();
                  setState(() {
                    text = "Start Service";
                  });
                } else {
                  service.startService();
                  setState(() {
                    text = "Stop Service";
                  });
                }
              },
            ),
            const Expanded(
              child: LogView(),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  State createState() => _LogViewState();
}

class _LogViewState extends State {
  String logs = '';

  @override
  void initState() {
    super.initState();
    FlutterBackgroundService().on('log').listen((event) {
      setState(() {
        logs += '${DateTime.now()} $eventn';
      });
      if (Platform.isAndroid) {
        print(event);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      child: SingleChildScrollView(
        child: Text(logs),
      ),
    );
  }
}

Handling Permissions

When working with background services and plugins, ensure you handle necessary permissions for Android and iOS. Request runtime permissions when needed and handle cases where permissions are denied.

Using WorkManager (Android Only)

WorkManager is an Android Jetpack library for scheduling deferrable, guaranteed background work. It’s designed to execute tasks even if the app is closed or the device restarts.

Step 1: Add the Dependency

Add the WorkManager dependency to your build.gradle file:

dependencies {
    implementation "androidx.work:work-runtime-ktx:2.7.1"
}

Step 2: Create a Worker Class

Create a worker class that extends Worker and overrides the doWork() method:

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters

class MyWorker(appContext: Context, workerParams: WorkerParameters):
    Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // Perform background task here
        return Result.success()
    }
}

Step 3: Schedule the Work

Schedule the work using WorkManager:

import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager

fun scheduleWork(context: Context) {
    val myWorkRequest = OneTimeWorkRequestBuilder().build()
    WorkManager.getInstance(context).enqueue(myWorkRequest)
}

Conclusion

Background operations are essential for modern Flutter applications, allowing you to perform tasks seamlessly without interrupting the user experience. By utilizing services and plugins effectively, you can build robust and efficient background processes. Choose the method that best suits your application’s requirements, and remember to handle permissions and lifecycle events properly to ensure your background tasks run reliably.