In mobile application development, especially with Flutter, handling tasks in the background such as fetching location data and syncing with Bluetooth devices can be quite challenging. Flutter, being a cross-platform framework, abstracts away many platform-specific details, but this also means developers need to employ specific strategies to handle background operations effectively. In this blog, I’ll walk you through the process of implementing a Flutter application that fetches location data and syncs with a Bluetooth device even when the app is running in the background.


Case study


Recently I have worked on a project where the primary goal of the app is to help the forklift drivers do the work in specific geo-locations using AI .a mobile device will be connected to a BLE device & get a signal from an AI board. The app will then fetch location data & sync the signal data from the device. after that, it will hit a certain API with the parameters. This operation will be continued until the user turns off the driving mode. The operation should fetch the location & sync the data even if the app is in the background or the display is dimmed. Here comes the challenging part….



Understanding Background Features in Flutter



Flutter’s design inherently focuses on the foreground operations of an app, which is common for most user-interactive applications. However, for tasks that need to run continuously or periodically in the background, such as location tracking or Bluetooth communication, we need to dive deeper into platform-specific implementations.



How Background Operations Work



Background operations can be categorized into:

1. Background Fetch: A process that runs periodically to fetch data.

2. Foreground Service: A continuous service that keeps running even when the app is not in the foreground. This is common for tasks like location tracking or continuous data sync.

3. Background Service: Tasks that are executed when the app is not visible to the user and do not require continuous execution.



Approaching Background Tasks in Flutter



To handle background tasks, Flutter provides several options:

Method Channels: For executing platform-specific code.

Plugins: Various plugins exist to help with background tasks, such as `geolocator` for location tracking and `flutter_background_service` for creating foreground services.



Implementing Background Location Fetch and Bluetooth Sync



For our project, we required two main functionalities:


1. Fetching location data in the background.

2. Syncing data with a Bluetooth device continuously.


Here’s a step-by-step guide to achieving this using the `geolocator` package for location and `flutter_background_service` for maintaining a service that runs in the background.

the approach I have taken is that I will show a local notification when the app is in the background & dismiss it when it comes to the foreground or resumes again. The local notification will also display the signal it gets from the device so that the user may know the signal info even if the app is in background.



Step 1: Setting Up the Project



First, create a new Flutter project or use an existing one. There were a lot more in the code but i will just focus on the background task part. Add the necessary dependencies in your `pubspec.yaml` file:


dependencies:

geolocator: ^11.0.0

permission_handler: ^11.3.1

flutter_local_notifications: ^17.1.2

flutter_background_service: ^5.0.5



Step 2: Configuring Method Channel



Method channels allow you to communicate between Flutter and the native platform (iOS and Android). We will use method channels to initiate and control the foreground service.

Create a new file `background_service_helper.dart` & add this code


class BackgroundServiceHelper {

  static Future<void> onStart(ServiceInstance service) async {
    if (service is AndroidServiceInstance) {

      service.setForegroundNotificationInfo(
        title: "Background Service",
        content: "Fetching location and signal",
      );
    }

    service.on('stopService').listen((event) {
      service.stopSelf();
    });

  }
}



Step 3: Implementing Foreground Service in Android



In your `MainActivity.kt` or `MainActivity.java`, add the following code to handle the method channel and start the foreground service:


class MainActivity : FlutterActivity() {

    private val CHANNEL = "Your package name/permissions"
    private var engine: FlutterEngine? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        engine = flutterEngine

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "requestForegroundServicePermissions") {
                requestForegroundServicePermissions(result)
            } else {
                result.notImplemented()
            }
        }
    }

    private fun requestForegroundServicePermissions(result: MethodChannel.Result) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            requestPermissions(arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.FOREGROUND_SERVICE
            ), 1)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            requestPermissions(arrayOf(
                Manifest.permission.FOREGROUND_SERVICE_LOCATION
            ), 1)
        }
        result.success(true)
    }

    @Override // This annotation is not required in Kotlin
    override fun onDestroy() {
        super.onDestroy()
        engine?.destroy()
        engine = null
    }
}


Declare it in the manifest file


<service
    android:name="id.flutter.flutter_background_service.BackgroundService"
    android:foregroundServiceType="location | dataSync"
    android:permission="android.permission.BIND_JOB_SERVICE" />



Step 4: Fetching Location in the Background



Utilize the `geolocator` package to fetch location data. we have used the getx for state management, so writing the methods in controller. Here I will only mention the necessary functions. we have started the service in the controller class, when the app goes background the service starts & will show the local notification banner. we have used ‘flutter_local_notifications’ package for showing local notifications.

here are the codes for the controller

Initialize the method channel


  static const MethodChannel _methodChannel =
      MethodChannel('you package name/permissions');


We have to ask for the permissions first


Future<void> initializePermissions() async {
  final locationStatus = await Permission.location.status;
  if (locationStatus.isDenied) {
    await Permission.location.request();
    return; // Exit after requesting location permission
  }

  // Request notification and ignoreBatteryOptimizations sequentially
  final notificationStatus = await Permission.notification.request();
  if (notificationStatus.isDenied) {
    Get.snackbar(
      "Permissions Required",
      "This app needs Notification permission to function properly. Please grant it in your app settings.",
      snackPosition: SnackPosition.BOTTOM,
    );
    return; // Exit after requesting notification permission
  }

  final ignoreBatteryStatus =
      await Permission.ignoreBatteryOptimizations.request();
  if (ignoreBatteryStatus.isDenied) {
    Get.snackbar(
      "Permissions Required",
      "This app needs Ignore Battery Optimizations permission to function properly. Please grant it in your app settings.",
      snackPosition: SnackPosition.BOTTOM,
    );
  }

  if (Platform.isAndroid == true) {
    try {
      await _methodChannel
          .invokeMethod('requestForegroundServicePermissions');
    } on PlatformException catch (e) {
      log("Error requesting foreground service permissions: $e");
    }
  }
}


in modern Android devices, the OS sometimes kills the app in the background if it drains more battery. so ‘Ignore_battery_status’ permission is necessary. Users can manually turn on the permission too. Then if the OS is Android, we will invoke the method channel.

Then we can initialize the local notification as needed.


void initializeNotifications() {
  final initializationSettingsAndroid =
      const AndroidInitializationSettings('@mipmap/ic_launcher');

  const DarwinInitializationSettings initializationSettingsDarwin =
      DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );

  final InitializationSettings initializationSettings =
      InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsDarwin,
  );

  flutterLocalNotificationsPlugin.initialize(initializationSettings,
      onDidReceiveBackgroundNotificationResponse:
          handleBackgroundNotificationClick);
}


it also has some methods to handle the click event


static Future<void> handleBackgroundNotificationClick(
      NotificationResponse notificationResponse) async {
    await handleNotificationClick(notificationResponse.payload);
  }
static Future<void> handleNotificationClick(String? payload) async {
    if (payload != null) {
      //do your necessary actions here
    }
  }
Future<void> showNotification(String status) async {
  const AndroidNotificationDetails androidPlatformChannelSpecifics =
      AndroidNotificationDetails('notification channel id', 'notification name',
          importance: Importance.max,
          priority: Priority.high,
          showWhen: false,
          ongoing: false);

  const DarwinNotificationDetails darwinPlatformChannelSpecifics =
      DarwinNotificationDetails();

  const NotificationDetails platformChannelSpecifics = NotificationDetails(
      android: androidPlatformChannelSpecifics,
      iOS: darwinPlatformChannelSpecifics);

  FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  await flutterLocalNotificationsPlugin.show(
      1, // notification id
      'title', // notification title
      status, // notification body
      platformChannelSpecifics,
      payload: "payload"); // This will help to catch the payload data
}



This ‘showNotification’ method is for showing notification. you call it anywhere where you want to trigger the notification. i have triggered it during the background service. first, i have to detect the app life cycle state using ‘WidgetsBindingObserver’ and then use it appropriately.





@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  super.didChangeAppLifecycleState(state);

  if (state == AppLifecycleState.paused) {
    log("APP is paused");
    
    inBackground = true;

    if (inBackground) {
      showNotification("백그라운드 모드에서 작동 중 입니다");
      initializeService();
      FlutterBackgroundService().startService();
    }

     } else if (state == AppLifecycleState.resumed) {
    log("APP is resumed");

    FlutterBackgroundService().invoke("stopService");
    
    inBackground = false;

    FlutterLocalNotificationsPlugin().cancelAll();

 
  }

}



here when we detect the app is in the foreground, we start the service & show the local notification. Again, when the app is in a foreground state, we stop the service. we call the stop service also in dispose() & onClose() method more critical scenarios.


void initializeService() async {
  final service = FlutterBackgroundService();
  await service.configure(
    androidConfiguration: AndroidConfiguration(
      onStart: onServiceStart,
      autoStart: true,
      isForegroundMode: true,
    ),
    iosConfiguration: IosConfiguration(
      onForeground: onServiceStart,
      onBackground: onIosBackground,
    ),
  );
}
static void onServiceStart(ServiceInstance service) async {
  BackgroundServiceHelper.onStart(service);
}


to stop all notifications & services we call this method


void cancelNotificationService() {

  FlutterLocalNotificationsPlugin().cancelAll();
  FlutterBackgroundService().invoke("stopService");
}



Step 5: Syncing with Bluetooth Device



To sync with a Bluetooth device, use the appropriate Bluetooth package and integrate it within the `LocationService` class. Ensure you handle the Bluetooth operations efficiently to maintain connection and data sync.



Step 6: Permissions



Don’t forget to request the necessary permissions in your Android manifest file (`AndroidManifest.xml`):


<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />


There are additional setups for ios. in plist file add the necessary strings.


<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to external devices for data synchronization and device control, even when the app is in the background.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to connect to external devices for data synchronization and device control, even when the app is in the background.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app requires access to your location to provide location-based services, such as tracking your movements and sending location-based notifications, even when the app is in the background.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app requires access to your location to provide location-based services, such as tracking your movements and sending location-based notifications, even when the app is in the background.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app requires access to your location to provide location-based services while the app is in use, such as tracking your movements and sending location-based notifications.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
   <key>UIBackgroundModes</key>
   <array>
     <string>fetch</string>
     <string>remote-notification</string>
   </array>
   <key>BGTaskSchedulerPermittedIdentifiers</key>
   <array>
       <string>dev.flutter.background.refresh</string>
   </array>
<key>UIBackgroundModes</key>
<array>
   <string>fetch</string>
   <string>location</string>
   <string>remote-notification</string>
</array>
<key>UIBackgroundModes</key>
<array>
   <string>bluetooth-central</string>
</array>


Please see the package documentation for further details.


Conclusion


Implementing background tasks in Flutter requires a blend of Dart and platform-specific code. Using method channels and foreground services, we can achieve continuous location tracking and Bluetooth data synchronization even when the app is in the background. Combining the `geolocator` and `flutter_background_service` packages provides a robust solution for such requirements.

This approach ensures that your application remains responsive and functional, providing users with the essential features they need without interruption.

Thank you for reading till the end. let me know in the comments if you have any suggestions to make it more efficient or any other proper approach.



Package links:


https://pub.dev/packages/flutter_background_service

https://pub.dev/packages/geolocator

https://pub.dev/packages/flutter_local_notifications

Leave a Comment