Background Services in Flutter Add-to-App Case

Rating: 4.35 / 5 Based on 20 reviews

Using background processes in Flutter apps is common. However, when adding Flutter to existing native apps, things can get more complicated. As part of a proof-of-concept that we developed for a client from the banking sector, we had to implement a business process that required some work to be performed in the background in a Flutter module. This module was to be injected into native Android and iOS apps. See our case and code examples of implementing background services in Flutter add-to-app.

If you want to learn more about Flutter add to app read our previous article.

Background processes - case description

Mobile apps sometimes have to perform some operations in the background. It is a non-trivial technical challenge to make that work reliably, and some restrictions on the operating system side must always be kept in mind. 

Background processing covers many interesting use cases in mobile apps, such as:

  • Activity tracking (fitness apps)
  • Location tracking
  • Media playback (including voice or video calls)
  • Data downloading and uploading (uploading big files can take a lot of time)
  • Communication with Bluetooth devices (e.g., beacons)
  • Asynchronous business processes (performing some business logic in the background or asynchronously reacting to some changes)

Let’s imagine the following generic scenario. The user navigates the native part of the app from which they can open a Flutter screen. On the Flutter screen is a button to initiate background processing (think long-term calculations or data synchronization). This button starts a background process which will continue running even after the user closes the app. 

When the background process is finished, we want to show another Flutter screen, this time some dialog. This dialog can appear anywhere in the app (both over a native or a Flutter screen). If the app is not in the foreground, we can show a push notification that will open the dialog screen (this is a more typical scenario).

Adding background services - a concept of Dart isolates

We need some form of concurrency to implement that process. Because we’re dealing with the Flutter add-to-app scenario, the navigation paths of native and Flutter screens can cross. That means that we need multiple execution environments for Flutter - we won’t be able to reuse a single Flutter engine for the two screens and the background service.

Dart supports concurrency with the concept of isolate. Isolates use a single thread of execution and don’t share any mutable objects with other isolates. Separate memory is where isolates differ from traditional threads known from other languages. Each isolate processes events in its event loop. Isolates can communicate between themselves by passing messages via so-called ports.

Therefore, in our case, we will need three isolates:

  • main isolate - which shows the first Flutter screen;
  • background isolate - which performs calculations in the background;
  • dialog isolate - which shows the dialog after the calculations are finished.

The Flutter add-to-app approach is based on the notion of FlutterEngine - the execution environment that is running inside the native app. If we want to have multiple isolates, we need multiple engines. Recently, the Flutter team added a new concept called FlutterEngineGroup, which allows engine instances to share memory resources, significantly decreasing resource consumption. We are going to leverage this addition to make our solution more performant. Each isolate will run by a separate engine in the group.

Background services - Flutter add-to-app and native code

Let’s create base classes for hosts and clients - they will serve as an abstract communication protocol for our isolates. Hosts will register isolates and receive messages sent by other isolates. Other isolates will use clients to send messages to the isolate. We will make those types generic so that isolates can define their message contracts.

abstract class IsolateClient<T> {
  IsolateClient(this._isolateName);
 
  final String _isolateName;
 
  
  SendPort? get sendPort => IsolateNameServer.lookupPortByName(_isolateName);
 
  
  void send(T message) => sendPort?.send(message);
}
 
const _isolateName = 'mainIsolate';
 
abstract class MainIsolateMessage {}
 
class BackgroundServiceStarted extends MainIsolateMessage {}
 
class MainIsolateClient extends IsolateClient<MainIsolateMessage> {
  MainIsolateClient() : super(_isolateName);
}
 
class MainIsolateHost extends IsolateHost<MainIsolateMessage> {
  MainIsolateHost._(ReceivePort port)
    : super(receivePort: port, isolateName: _isolateName);
 
  factory MainIsolateHost.register() {
    return MainIsolateHost._(registerIsolate(_isolateName));
  }
}

We will also need to define a communication protocol between Dart and native code with method channels. We will use the pigeon package to avoid writing boilerplate code in Dart, Swift, and Kotlin. It is a code generator for generating typesafe contracts for communication between Flutter and the host platform. With Pigeon, you create a Dart schema file, and the package generates Dart, Objective-C, and Java files with models and method channel invocations. 

This will be our Dart schema:

()
abstract class NativeMainApi {
  void startService(ComputationNotification notification);
  void stopService();
}
 
()
abstract class NativeDialogApi {
  void closeDialog();
}
 
()
abstract class NativeBackgroundServiceApi {
  void stopService();
  void openDialog();
  void updateNotification(ComputationNotification notification);
}
 
class ComputationNotification {
  late final String title;
  late final String message;
  late final int percentProgress;
}

HostApis define a native interface accessible from Dart code. Pigeon also offers FlutterApis for communication from native platforms to Flutter, but for simplicity’s sake, in this example, we’re only going to use single-way communication. We define separate APIs for each isolate

The main isolate will be able to start and stop the computation. The background service isolate defines an interface for stopping the native service, opening a Flutter dialog, and updating a notification shown in the system tray. The dialog isolate can close itself (and destroy the native Flutter engine).

Each isolate needs a separate entrypoint (the primary function). We can define them in separate files or a single file. Still, in the latter case, we need to remember to add the pragma annotation so that this function name won’t get stripped or get its name changed during compilation optimizations, and the native code can call it.

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MainApp());
}
 
('vm:entry-point')
void backgroundServiceMain() {
  WidgetsFlutterBinding.ensureInitialized();
  startBackgroundService();
}
 
('vm:entry-point')
void dialogMain() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const DialogApp());
}

Background services in native iOS app

On iOS, we used background tasks - the case was simple enough (the process was time-limited), so they fulfilled our requirements. For more complex scenarios, you can resort to the Background Fetch API - the implementation would be similar.

This piece of code starts up a Dart service and begins a background task so that it will continue executing in the background.

private let dartEntrypoint = "backgroundServiceMain"
 
func start(notification: LNCDComputationNotification) {
    if engine == nil {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        let engine = appDelegate.flutterEngineGroup.makeEngine(withEntrypoint: dartEntrypoint, libraryURI: nil)
				engine.run()
    }
      
    backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(
	    withName: backgroundTaskName, 
        expirationHandler: {
           self.stop()
    })
    lastNotification = notification
}

Background services in native Android app

We will base our Android implementation on the foreground service - a type of service that displays a system notification so that the user is aware that the app is performing some work when they are not interacting with it. An implementation using a background service would be analogous.

In total, we will use 3 activities:

  • Native MainActivity, which has a button to redirect us to a Flutter activity,
  • FlutterMainActivity with a screen showing a button to start the computation,
  • FlutterDialogActivity for showing a Flutter dialog when the computation completes.

The service will start Dart code in the following way:

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (engine == null) {
        ComputationServiceNotification.createNotificationChannel(applicationContext)
        val notification = ComputationServiceNotification.createNotification(applicationContext, intent!!)
        startForeground(SERVICE_ID, notification)
        startDartService()
    }
    return super.onStartCommand(intent, flags, startId)
}
   
private fun startDartService() {
    engine = FlutterUtils.createOrGetEngine(this, AppFlutterEngine.computationService)
    engine!!.let {
        Api.NativeBackgroundServiceApi.setup(it.dartExecutor, ComputationServiceApiHandler(this))
        api = Api.FlutterMainApi(it.dartExecutor)
    }
}

First, we see if the engine is not running already. Then, we create a notification, start the foreground service and create a FlutterEngine which will execute Dart code in the background.

The whole setup requires some boilerplate. You can check out the complete example here.

Summary of background processing implementation

This article showed you how to implement background processing in native apps with added Flutter. While the implementation is not trivial and has many gotchas, the most important thing we have shown is that you can keep the whole core business logic multiplatform in Dart. Native code is mostly infrastructural and generic. It is something necessary because we need to integrate Flutter with native apps. 

When hearing about doing something in the background, especially in the add-to-app, one could quickly start looking at doing a native implementation. We can really leverage Flutter here and have the business logic written and tested once, and be assured that it works the same way on the Android and iOS applications.

If you need help with the Flutter add-to-app, you can reach out to our team.

Meet our expert

We can help you build your app!

Send us a message, and we will find a solution and service that suits you best.
Rate this article
4.35 / 5 Based on 20 reviews

Read more

Flutter has taken the mobile market by storm, but not everybody knows that you don’t always have to write a Flutter app from scratch. It can be integrated into your existing application piecemeal. Read more about the Flutter add to app feature.
Flutter Add to App
One of the projects we delivered at LeanCode was building an application for Healthcare Industry - Quittercheck. While developing the app, the problem we faced was how to limit a video upload to a specific time frame. Read about our tailored solution to this challenge.
Limiting a Video Upload to a Specific Time Frame
Flutter is loved by many for its simple and fast development of high-quality cross-platform apps. Let’s take a closer look if Flutter is a suitable solution in every case, i.e., when developing mobile, web, and desktop applications.
Is Flutter good for app development?