Skip to content
Back to archipelago.banuacoder.com

Infrastructure SDKs

background_sdk Free ​

Tier
Freeversion 0.1.4
Command
archipelago generate background_sdk
Generates
  • infrastructure/background_sdk_api
  • infrastructure/background_sdk_workmanager

Background work on WorkManager. Features register task handlers in a registry, and each task run gets its own DI container on the worker isolate, torn down when the run ends.

Variables ​

This brick has no generation-time variables.

Install ​

Interactive ​

bash
archipelago generate background_sdk

Non-interactive (CI) ​

bash
archipelago generate background_sdk --config my_config.json

Why This Exists ​

The OS starts a background task on an isolate without the app. On that isolate:

  1. GetIt.instance is empty. A class that reads it throws Object/factory not registered, at runtime, in release, only when the task fires.
  2. A database connection opened by the UI isolate cannot be used. The task must open its own.
  3. A connection the task forgets to close can hold the database's write lock and block the UI isolate.

So a class the task runs must take its collaborators through its constructor, and the task needs its own short-lived container. This brick generates that composition root, so the app writes only the handler body.

Generated Files ​

PathPurpose
infrastructure/background_sdk_api/BackgroundTaskRegistry, BackgroundTaskHandler, BackgroundTaskResult, BackgroundScheduler. Features depend on this package only.
infrastructure/background_sdk_workmanager/BackgroundWorker (one run in its own container), runWorkmanagerDispatcher, WorkmanagerBackgroundScheduler
apps/<app>/lib/background/background_dispatcher.dartThe @pragma('vm:entry-point') dispatcher, setupBackgroundDi and registerBackgroundTasks. You edit these two functions.
apps/<app>/lib/host/initializers/background_work_initializer.dartPre-launch: registers the dispatcher with WorkManager and BackgroundScheduler in getIt
apps/<app>/test/background/background_dispatcher_test.dartFails if the composition root reads the app-wide GetIt

The post-gen hook adds both packages to the workspace and the app's pubspec.yaml, and adds BackgroundWorkInitializer to the pre_launch_initializers marker region.

How a Task Runs ​

sequenceDiagram
    participant OS as WorkManager (OS)
    participant D as backgroundDispatcher
    participant W as BackgroundWorker
    participant R as BackgroundTaskRegistry
    participant DI as Task container
    participant H as Feature handler
    OS->>D: start isolate, executeTask(task, input)
    D->>W: run(task, input)
    W->>R: registerBackgroundTasks(registry)
    W->>R: handlerFor(task)
    alt no handler
        W-->>D: failure (error reported)
    else handler found
        W->>DI: asNewInstance() + setupBackgroundDi(di)
        W->>H: handler(di, input)
        H-->>W: success / retry / failure (a throw is failure)
        W->>DI: reset() in finally, dispose callbacks run
        W-->>D: result
    end
    D-->>OS: true = success, false = retry, throw = failure
SituationResultError reported
Handler returns successsuccessno
Handler returns retryretry, with the OS backoffno
Handler returns failurefailure, no retryno
Handler or setup throwsfailure, no retryyes
No handler for the taskfailure, no retryyes

A throw is treated as a bug, not a transient error, so it is not retried. Catch transient errors (no network, a locked file) in the handler and return retry.

Errors go to BackgroundWorker's onError, which logs through dart:developer by default. To send them to crash reporting, pass your own onError in background_dispatcher.dart.

Writing a Handler ​

In the feature, export a registration function. It depends on background_sdk_api only:

dart
// features/sync/sync_impl/lib/src/background/sync_background_tasks.dart
const syncTask = 'sync.pull';

void registerSyncBackgroundTasks(BackgroundTaskRegistry registry) {
  registry.register(syncTask, (di, input) async {
    final pull = PullChanges(di<SyncApi>(), di<AppDatabase>());
    try {
      await pull();
      return BackgroundTaskResult.success;
    } on SocketException {
      return BackgroundTaskResult.retry;
    }
  });
}

In the app, register what the handlers need and call the feature's function:

dart
// apps/<app>/lib/background/background_dispatcher.dart
Future<void> setupBackgroundDi(GetIt di) async {
  di.registerSingletonAsync<AppDatabase>(
    AppDatabase.openInBackground,
    dispose: (db) => db.close(),
  );
  di.registerLazySingleton<SyncApi>(() => SyncApi(Dio()));
  await di.allReady();
}

void registerBackgroundTasks(BackgroundTaskRegistry registry) {
  registerSyncBackgroundTasks(registry);
}

Schedule from the main isolate through the port, never through Workmanager directly:

dart
await getIt<BackgroundScheduler>().schedulePeriodic(
  syncTask,
  const Duration(hours: 1),
);

The task name is also WorkManager's unique name, so scheduling a pending task again replaces it, and iOS periodic tasks reach the same handler.

Rules for code the task runs ​

  • Never read getIt or GetIt.instance. Resolve from di in the handler, then pass objects through constructors.
  • Register only isolate-safe things in setupBackgroundDi: no widgets, no BuildContext, no plugin that needs an Activity (pickers, permission prompts, in-app purchase).
  • Open the database on this isolate. Give every resource a dispose: so the reset closes it.

Platform setup ​

The brick does not edit native projects:

  • Android: no setup for one-off and periodic tasks.
  • iOS: periodic and processing tasks need UIBackgroundModes (fetch, processing) and the task names in BGTaskSchedulerPermittedIdentifiers in Info.plist. See the workmanager iOS setup.

Limits ​

  • The generated architecture test checks the files in lib/background/ and the two packages. It does not follow every class a handler reaches. Keep handler classes constructor-injected.
  • There is no database brick yet. setupBackgroundDi shows where to open one.