How to run a background job when your Flutter app is terminated.

If you have always wondered how apps like google photos backup your media even when the app is in terminated mode (you have not opened the app). My first guess was that they must run it in the background through workmanager (android). But the workmanager doesn't work when the app is terminated.

Recently I discovered the concept of silent notification, a silent notification doesn't show any icon in the notification tray or make any system sound. But the app can listen to this notification even when the app is terminated. Like how you get the usual notification from Instagram, WhatsApp even when the app is terminated. In our case instead of showing a notification popup we will run a function, which can be anything from syncing contacts, media, etc.

Prior knowledge of Firebase notification and some familiarity with writing backend is necessary to implement this

Here we are doing the basics of setting up firebase, if you don't know how to setup firebase in flutter there are tons of good articles and blogs on how to do it.

The FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); listen to notification events sent to the app when the app is in the background. fcmToken = await FirebaseMessaging.instance.getToken(); The fcmToken is a unique token generated by firebase on its own, we will use this to send notifications to the particular device.

this function should come before the main.

this is how it should be above the main function. This is a good enough setup in ur flutter app for the silent notification. If you have worked we flutter notifications before you must be aware you can send notifications from the firebase console too. But silent notification needs some extra settings which are not available through the console. So we have to setup a backend.

we setup express with firebase-admin (firebase-admin is a npm package). serviceAccount = require("./flutter-firebase-learn-5cce0-firebase-adminsdk-2xepr-5efb8a547e.json"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), });

here serviceAccount is nothing but a firebase json file, In your respective firebase project go to project setting > service account

we create a post request /sendSilentNotification , it takes a registrationToken, we pass the fcmToken we got before in the registrationToken, const message = { data: { silent: "true", }, };

make sure to pass true as a string in silent and not boolean.

As the rest of the whole code is self-explanatory, you can find the whole backend code below

const express = require("express");
const admin = require("firebase-admin");

// Initialize the Firebase admin SDK
var serviceAccount = require("./flutter-firebase-learn-5cce0-firebase-adminsdk-2xepr-5efb8a547e.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});

const app = express();
const port = 3000;

app.use(express.json());

// Endpoint to trigger a silent push notification
app.post("/sendSilentNotification", (req, res) => {
  const { registrationToken } = req.body;

  console.log(req.body);
  if (!registrationToken) {
    res.status(400).json({
      success: false,
      error: "Invalid request. Either topic or registrationToken is required.",
    });
    return;
  }

  const message = {
    data: {
      silent: "true",
    },
  };

  if (registrationToken) {
    message.token = registrationToken;
  }

  console.log(message);
  // Send the silent notification using FCM
  admin
    .messaging()
    .send(message)
    .then(() => {
      res.status(200).json({
        success: true,
        message: "Silent notification sent successfully",
      });
    })
    .catch((error) => {
      console.error("Error sending silent notification:", error);
      res
        .status(500)
        .json({ success: false, error: "Failed to send silent notification" });
    });
});

// Start the server
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

we modify our flutter _firebaseMessagingBackgroundHandler to do a specific task when we receive a silent notification


@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  if (message.data["silent"] == "true") {
    Download().saveNetworkImage();
  }
  print("Handling a background message: ${message.data}");
}

We want to download an image when we trigger a silent notification and the app is in terminated mode (this can be any action you want to perform like contact sync, or media backup)

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';

class Download {
  void saveNetworkImage() async {
    String path =
        'https://image.shutterstock.com/image-photo/montreal-canada-july-11-2019-600w-1450023539.jpg';
    GallerySaver.saveImage(path, toDcim: true).then((bool? success) {
      print("success ${success}");
    });
  }
}

Future<void> downloadAndSaveImage(String imageUrl) async {
  try {
    Dio dio = Dio();

    String filename = "demo.jpg";

    String path = await _getFilePath(filename);
    // Download the image using Dio
    await dio.download(imageUrl, path);
    print(path);
  } catch (error) {
    print('Failed to download and save image: $error');
  }
}

Future<String> _getFilePath(String filename) async {
  final dir = await getApplicationDocumentsDirectory();
  return "${dir.path}/${filename}";
}

this is the download function.

Now it's time to test,

Run your flutter app with flutter run --release this command will build your app so that when you terminate the app you will still check the logs of what is happening in the app. After running the app, run your backend on localhost, I will be using postman to send the request

before sending the request, make sure to terminate the app.

and in your app terminal you should be getting logs with the file location which shows that the image is downloaded, while the app is terminated, to get the downloaded image in your gallery we have to do some extra hacking, I will keep that for my next article.

Here you might be thinking to get this done the app needs to be terminated and then send the request, which is fine but what if the user is using the app or the app is in the background at that moment, how to handle that? It is exactly the same you just have to look up how to handle Firebase notifications in the foreground and in the background, the rest is the same.