Foreground Services in Flutter: Run tasks when your App is minimized

Punnyarthabanerjee
6 min readApr 3, 2024

--

Recently I encountered a problem while developing a system which required location data to be fetched when the app is minimised.

I could not find a relevant article that generalises the concept of running tasks when your app is minimized. You might need to run a lot of tasks when the user is not directly looking at the screen, like playing music, sending log data over to your backend and connecting to devices.

So I decided to write one myself!

Some Basics

Before we dive into the implementation, I want you to know some basics about services in Android.

Google describes a service as

An application component that can perform long-running operations in the background. It does not provide a user interface.

But I describe a service as

Something which took a lot of time and coffee to implement. Please provide better documentation Google !

There are 3 types of services in Android.

  1. Foreground:- This service is used when the app is in the minimized state.
  2. Background:- This service is used when the app is in the terminated state.
  3. Bound:- This service is used to bind more than one activity and can be used for communication between multiple apps. In other words, it is too complicated for this article :)

We will be creating foreground services in this article.

Why are we not using the background service?

The background service does not notify the user that it is running and it is likely to get terminated by the Android system when the system requires memory. Therefore you cannot make sure your service or task is running as per your wish.

The foreground service on the other hand notifies the user using a notification and is kept in priority by the Android system. Therefore it will not be terminated by the system.

This is how it works.

Implementation

Yaay !

Here we require the following Flutter libraries

  1. flutter_foreground_task
  2. awesome_notifications ( you can also use flutter_local_notifications )
  3. permission_handler for asking for permissions when a user opens the app

Configuration

You can look at the configuration below or directly from the flutter_foreground_task library.

We have to let the Android system know what functionalities we are going to use in the foreground service. Here is a list of permissions.

First, we declare the permissions and services in the AndroidManifest.xml file located in android/app/src/main directory.

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

<!-- Here we put in permissions -->

<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

<!-- This is required -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<!-- Choose this based on the foreground services list from above -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />


<application
android:label="testapp"
android:name="${applicationName}"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/launcher_icon">

<!-- This is required -->
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:stopWithTask="false" <!-- Set this to true if you want your service to be stopped with your task-->
android:foregroundServiceType="connectedDevice" <!--Put whatever type you want from the foreground services list given above -->
android:exported="false" />

<!-- Rest is same -->
<activity
android:name=".MainActivity"
android:exported="true"
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>
</manifest>

We can now move on!

Create a services folder in your lib/ directory and a file named foregroundService.dart in it. (You can name it anything, just follow principles)

Now open it, write the following code and customize it according to your needs.

import 'dart:async';
import 'dart:isolate';

import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';

class ForegroundTaskService{
static init(){
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'foreground_service',
channelName: 'Foreground Service Notification',
channelDescription: 'This notification appears when the foreground service is running.',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
iconData: const NotificationIconData(
resType: ResourceType.mipmap,
resPrefix: ResourcePrefix.ic,
name: 'launcher',
),
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: true,
playSound: false,
),
foregroundTaskOptions:const ForegroundTaskOptions(
isOnceEvent: true,
),
);
}
}

This initializes our foreground service settings.

Now we create a function that will be called on invoking our foreground service. (If you are confused about invoking, we will come to that later in this article)

Write the following in the same file below the ForegroundTaskService class. Feel free to change it to your needs.


@pragma('vm:entry-point') // This decorator means that this function calls native code
void startCallback() {
FlutterForegroundTask.setTaskHandler(FirstTaskHandler());
}

class FirstTaskHandler extends TaskHandler {
SendPort? _sendPort;

// Called when the task is started.
@override
void onStart(DateTime timestamp, SendPort? sendPort) async {
_sendPort = sendPort; // This is used for communicating between our service and our app
sendPort?.send("startTask");
}

// Called every [interval] milliseconds in [ForegroundTaskOptions].
@override
void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {
// Send data to the main isolate.

}

// Called when the notification button on the Android platform is pressed.
@override
void onDestroy(DateTime timestamp, SendPort? sendPort) async {
_timer?.cancel();
FlutterForegroundTask.stopService();
}

// Called when the notification button on the Android platform is pressed.
@override
void onNotificationButtonPressed(String id) {
_sendPort?.send("killTask");
}

// Called when the notification itself on the Android platform is pressed.
//
// "android.permission.SYSTEM_ALERT_WINDOW" permission must be granted for
// this function to be called.
@override
void onNotificationPressed() {
_timer?.cancel();
_sendPort?.send('onNotificationPressed');
FlutterForegroundTask.stopService();
}
}

For this article, I will only be using the onStart method.

Now create another folder called tasks under the lib/ folder and create sometask.dart. This is where we will write the code we want to execute (getting location etc).

class SomeTask{
Timer? _timer;
void performTask(){

// Do some task
Timer.periodic(duration: Duration(seconds:5),(){
print("Executing in foreground !");
});
}
void killTask(){
_timer?.cancel();
}
}

Now let's go to the main.dart file

Call the init function we created like this.

void main() {
WidgetsFlutterBinding.ensureInitialized();
// Request for permissions that you need
Permission.location.request();

ForegroundTaskService.init();
runApp(const MyApp());
}

And wrap whatever widget you want to keep running with WithForegroundTask.

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test',
home: const WithForegroundTask(child: MyHomePage(title: 'Foreground Service')),
);
}
}

Now we go to our desired widget (In my case MyHomePage)

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
// First we declare a receiverPort
ReceivePort? receivePort;
late SomeTask taskObj;
@override
void initState(){
super.initState();

// This is used to listen to the messages that senderPort sends from ForegroundTaskService we created
receivePort = FlutterForegroundTask.receivePort;
taskObj = SomeTask();
if (receivePort != null){
receivePort.listen((data){
if(data == "startTask"){
taskObj.performTask();
}
else if (data == "killTask"){
taskObj.killTask();
}
});
}
}


void startService()async{
if (await FlutterForegroundTask.isRunningService) {
FlutterForegroundTask.restartService();
}
else {
FlutterForegroundTask.startService(
notificationTitle: 'Foreground Service is running',
notificationText: 'Tap to return to the app',
callback: startCallback, // Function imported from ForegroundService.dart
);

}
@override
Widget build(BuildContext context) {
return WhateverWidget();
}
}

Now call the startService function from anywhere in your widget.

You will see a notification appearing on your taskbar and your task running!

To terminate the service, you can use

FlutterForegroundTask.stopService();

Yaay!

We can now successfully run any task without the user having to interact with the app!

Thank you for your time. Let me know your thoughts!

If you find it useful you can follow me on LinkedIn.

--

--