Triggering Code on Flutter’s Main Isolate from a WorkManager Background Isolate

Use Case: Managing Nutrition in a Game

I'm currently developing a game where nutrition plays a vital role. The user has a nutrition level that decreases over time, whether the app is open or not. When the nutrition level drops too low, I want to notify the user, prompting them to open the app and consume food.

To achieve this, I need to periodically calculate the current nutrition level and send a notification if it's too low. This needs to happen even when the app is in the background or closed.

The Challenge: Isolate Communication in Flutter

Flutter uses isolates for concurrency, with each isolate having its own memory space. This means that isolates do not share data or instances of classes directly. The workmanager package allows us to run background tasks in a separate isolate. However, the challenge arises when we need to communicate between the background isolate (where the workmanager task runs) and the main isolate (where the UI is managed).

For example, if the background task runs while the app is closed, it can create its own instance of a NutritionService to calculate the nutrition level and send notifications. However, if the app is open, we want the main isolate to handle these calculations. The main reason for this is that the UI listens to the NutritionService instance on the main isolate, and using a separate instance on the background isolate won't update the UI.

Solution: Communicating Between Isolates

To handle this scenario, we can implement a communication mechanism between the background isolate and the main isolate. One approach is to use SendPort and ReceivePort, which are part of Dart's isolate communication API. Here’s a high-level overview of how this can be implemented:

  1. Set Up a ReceivePort in the Main Isolate:

    • The main isolate should create a ReceivePort and pass its SendPort to the background isolate when initializing the background task.
  2. SendPort from Background Isolate:

    • When the background task is triggered by the workmanager, it can send a message through the SendPort to the main isolate.
  3. Handle Messages in the Main Isolate:

    • The main isolate listens to the ReceivePort and triggers the necessary UI updates or service calls based on the message received from the background isolate.
  4. Conditionally Execute Code:

    • If the app is open, the main isolate performs the necessary calculations and UI updates. If the app is closed, the background isolate handles the notification logic independently.

Here’s a simplified code outline for this:

// Main Isolate
void main() {
  final receivePort = ReceivePort();
  IsolateNameServer.registerPortWithName(receivePort.sendPort, 'main_send_port');

  receivePort.listen((message) {
    // Handle message from the background isolate
    if (message == 'trigger_nutrition_check') {
      // Perform nutrition check on main isolate and update UI
    }
  });

  runApp(MyApp());
}

// Background Isolate Task
void backgroundTask() {
  final sendPort = IsolateNameServer.lookupPortByName('main_send_port');

  if (sendPort != null) {
    sendPort.send('trigger_nutrition_check');
  } else {
    // If the main isolate is not available, run the task independently
    checkNutritionAndNotify();
  }
}

// Example function to register background task
void registerBackgroundTask() {
  Workmanager().registerOneOffTask('uniqueName', 'backgroundTask', inputData: {});
}

This method ensures that when the app is open, the main isolate handles the nutrition check, thereby keeping the UI in sync. If the app is closed, the background isolate takes over the task and sends a notification directly.

By handling isolate communication this way, we can ensure that the app's state remains consistent, and the user experience is smooth, regardless of whether the app is open or running in the background.

0
Subscribe to my newsletter

Read articles from Stephan E.G. Veenstra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Stephan E.G. Veenstra
Stephan E.G. Veenstra

Stephan is a Flutter Developer from The Netherlands. By day he is working on serious apps for Pinch, by night he likes to play around by making games and writing about what he has learned.