Perché servono i background task

Molte app moderne hanno bisogno di eseguire lavoro anche quando l'utente non sta interagendo con l'interfaccia: sincronizzare dati con un server, scaricare contenuti, inviare log, aggiornare una cache. In Flutter, l'isolate principale viene sospeso quando l'app passa in background, quindi non possiamo semplicemente lasciare un Timer in esecuzione.

La soluzione è affidarsi alle API native di scheduling del sistema operativo. Il plugin workmanager fa da ponte verso WorkManager su Android e BGTaskScheduler / performFetchWithCompletionHandler su iOS, permettendoci di registrare task che il sistema esegue rispettando i vincoli di batteria e rete.

Installazione

Aggiungiamo la dipendenza:

dependencies:
  workmanager: ^0.5.2

Il callback dispatcher

Il punto cruciale è che il task viene eseguito in un isolate separato, senza accesso allo stato dell'app in memoria. Per questo serve una funzione di top-level (o statica) annotata con @pragma('vm:entry-point'), necessaria affinché il codice non venga rimosso dal tree shaking in release.

import 'package:workmanager/workmanager.dart';

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    switch (task) {
      case 'sync-data':
        await _eseguiSincronizzazione(inputData);
        break;
      case 'cleanup-cache':
        await _pulisciCache();
        break;
    }
    // true = task completato con successo
    // false = il sistema riproverà seci sono criteri di retry
    return Future.value(true);
  });
}

Inizializzazione

L'inizializzazione va fatta nel main, prima di avviare l'app:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: true, // notifiche di debug, da disattivare in release
  );

  runApp(const MyApp());
}

Registrare un task one-off

Un task che deve essere eseguito una sola volta, con eventuali vincoli:

await Workmanager().registerOneOffTask(
  'task-unico-1',       // identificatore univoco
  'sync-data',          // nome usato nello switch
  inputData: {'userId': 42},
  initialDelay: const Duration(seconds: 10),
  constraints: Constraints(
    networkType: NetworkType.connected,
    requiresBatteryNotLow: true,
  ),
);

Task periodici

Per le sincronizzazioni ricorrenti usiamo registerPeriodicTask. Attenzione: su Android l'intervallo minimo imposto dal sistema è di 15 minuti, e iOS non garantisce una cadenza precisa ma decide in base ai pattern di utilizzo.

await Workmanager().registerPeriodicTask(
  'task-periodico-1',
  'sync-data',
  frequency: const Duration(hours: 1),
  constraints: Constraints(
    networkType: NetworkType.unmetered, // solo Wi-Fi
  ),
);

Annullare i task

// Annulla un task specifico
await Workmanager().cancelByUniqueName('task-periodico-1');

// Annulla tutti i task registrati
await Workmanager().cancelAll();

Configurazione iOS

Su iOS il setup richiede passaggi aggiuntivi. Nel file Info.plist bisogna abilitare le modalità di background:

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
  <string>com.tuodominio.app.sync</string>
</array>
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
  <string>processing</string>
</array>

E nell'AppDelegate.swift va registrato l'identificatore:

UIApplication.shared.setMinimumBackgroundFetchInterval(
  TimeInterval(60 * 15)
)

Best practice

  • Mantieni i task brevi: il sistema concede una finestra di esecuzione limitata. Operazioni troppo lunghe vengono terminate.
  • Inizializza tutto nel callback: l'isolate è isolato, quindi devi reinizializzare database, client HTTP e dipendenze dentro executeTask.
  • Gestisci gli errori: ritorna false per sfruttare la logica di retry, ma evita loop infiniti su errori non recuperabili.
  • Non aspettarti precisione su iOS: usa i background task per lavoro opportunistico, non per logiche critiche con scadenze rigide.
  • Testa su dispositivo reale: gli emulatori e le ottimizzazioni del produttore (es. Doze mode) influenzano l'esecuzione.

Conclusioni

Il plugin workmanager offre un'astrazione comoda sulle API native di scheduling, ideale per sincronizzazioni in background e manutenzione periodica. La chiave è progettare task idempotenti, brevi e indipendenti dallo stato dell'app, ricordando sempre i vincoli imposti dai sistemi operativi su batteria e rete.