| name | flutter-background-tasks |
| description | Flutter background task configuration guide for iOS (background_fetch/BGTaskScheduler) and Android (WorkManager). Use when setting up periodic background tasks, debugging background execution failures, or auditing platform configuration for background work. |
Flutter Background Tasks (iOS + Android)
Complete guide for configuring periodic background tasks in Flutter apps using workmanager (Android) and background_fetch (iOS).
Package Versions
dependencies:
workmanager: ^0.9.0
background_fetch: ^1.5.0
Architecture Overview
Foreground App Background Isolate
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ RevenueCat SDK โ โ SharedPreferences โ
โ SubscriptionProv โโโwritesโโโบ โ (cached status) โ
โ โ lobster_ โ โ
โ โ subscription_โโโreadsโโโบ Gate โ
โ โ cached โ .isSubscribed()
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
Background isolates CANNOT use:
- MethodChannel-based SDKs (RevenueCat, Firebase Auth listeners)
- EventChannel streams
- Platform views
Background isolates CAN use:
- SharedPreferences (read/write)
- HTTP requests (Dio, http)
- Firebase Core, Firestore, AppCheck (after re-init)
- Local file I/O
iOS Setup (background_fetch)
1. Info.plist (CRITICAL)
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.transistorsoft.fetch</string>
<string>com.transistorsoft.customtask</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
Common mistake: Forgetting com.transistorsoft.fetch causes BGAppRefreshTask to silently fail with no error logs. This is the #1 cause of iOS background tasks not executing.
2. Xcode Capabilities
In Xcode: Target > Signing & Capabilities > Background Modes:
3. AppDelegate.swift
No changes needed for iOS 13+. The SDK auto-registers from Info.plist.
If using workmanager for OTHER tasks (not background_fetch):
WorkmanagerPlugin.registerPeriodicTask(
withIdentifier: "com.example.myTask",
frequency: NSNumber(value: 24 * 60 * 60)
)
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
4. Podfile
The use_frameworks! line should work for most projects. If you encounter linking errors:
use_frameworks! :linkage => :static
Warning: Changing linkage can break other SDKs (Appodeal, AdMob). Test thoroughly.
5. iOS Constraints & Limitations
| Constraint | Value |
|---|
| Minimum interval | 15 minutes (iOS enforces, ignores higher values) |
| Background time limit | ~30 seconds |
| Scheduling control | Apple's ML algorithm decides when to run |
| After reboot | Automatically re-registered from Info.plist |
| App terminated | Works if enableHeadless: true |
| Must call finish | BackgroundFetch.finish(taskId) or task gets throttled |
| Custom taskId prefix | Must start with com.transistorsoft. |
6. iOS Timeout Handling
Always implement timeout with 2-second margin:
// iOS background time limit is ~30 seconds
// Use 28 seconds to leave margin for cleanup
final timeout = isIOSBackground ? 28 : 540; // seconds
await Future.any([
_doWork(),
Future.delayed(Duration(seconds: timeout)).then((_) {
throw TimeoutException('Background task timed out');
}),
]);
Android Setup (WorkManager)
1. AndroidManifest.xml
<manifest>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
...
</manifest>
2. build.gradle.kts
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
3. ProGuard (if using R8 minification)
WorkManager's AAR includes consumer ProGuard rules automatically. Usually no explicit rules needed. If you see reflection errors in release builds:
# Only if needed
-keep class androidx.work.** { *; }
-dontwarn androidx.work.**
4. Android Constraints & Limitations
| Constraint | Value |
|---|
| Minimum interval | 15 minutes (WorkManager enforces) |
| Background time limit | ~10 minutes |
| Network constraint | NetworkType.connected recommended |
| After reboot | Automatic (requires RECEIVE_BOOT_COMPLETED) |
| App terminated | Always works (WorkManager is OS-level) |
| Battery optimization | May be delayed by Doze mode |
| Backoff | Exponential recommended for network tasks |
Dart Implementation
Entry Point Functions
CRITICAL: Both callback functions MUST have @pragma('vm:entry-point') to prevent tree-shaking in release builds.
// Android WorkManager callback
@pragma('vm:entry-point')
void myCallbackDispatcher() {
Workmanager().executeTask((taskName, inputData) async {
// Re-initialize plugins in background isolate
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Do work...
return true; // success
});
}
// iOS background_fetch headless callback
@pragma('vm:entry-point')
void myHeadlessTask(HeadlessTask task) async {
final taskId = task.taskId;
final isTimeout = task.timeout;
if (isTimeout) {
BackgroundFetch.finish(taskId);
return;
}
// Re-initialize plugins
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Do work...
BackgroundFetch.finish(taskId); // MUST call or task gets throttled
}
Scheduler Pattern
class BackgroundScheduler {
final String taskName;
final String uniqueId;
Future<void> register({
required int intervalHours,
required Function callbackDispatcher,
Function(String)? iosCallback,
Function(HeadlessTask)? headlessTask,
}) async {
if (Platform.isAndroid) {
await Workmanager().initialize(callbackDispatcher);
await Workmanager().registerPeriodicTask(
uniqueId,
taskName,
frequency: Duration(hours: intervalHours),
constraints: Constraints(networkType: NetworkType.connected),
existingWorkPolicy: ExistingPeriodicWorkPolicy.replace,
backoffPolicy: BackoffPolicy.exponential,
initialDelay: const Duration(minutes: 1),
);
} else if (Platform.isIOS) {
await BackgroundFetch.configure(
BackgroundFetchConfig(
minimumFetchInterval: 15, // iOS minimum
stopOnTerminate: false,
enableHeadless: true,
),
iosCallback ?? (_) {},
(taskId) => BackgroundFetch.finish(taskId), // timeout handler
);
if (headlessTask != null) {
BackgroundFetch.registerHeadlessTask(headlessTask);
}
}
}
}
Subscription Gate Pattern (Background-Safe)
/// Reads cached subscription status from SharedPreferences.
/// Does NOT call any MethodChannel-based SDK.
class RevenueCatGate implements SubscriptionGate {
final AgentRepository _repo;
RevenueCatGate(this._repo);
@override
Future<bool> isSubscribed() async {
return _repo.getCachedSubscriptionStatus();
// Reads: prefs.getBool('${keyPrefix}_subscription_cached')
}
}
Foreground writes (in SubscriptionProvider):
// Write to SharedPreferences when subscription state changes
await prefs.setBool('${keyPrefix}_subscription_cached', isSubscribed);
Key consistency: The foreground write key and background read key MUST match exactly. Use the same keyPrefix pattern.
Debugging Background Tasks
iOS Simulator
Force-trigger background fetch:
xcrun simctl background_fetch <device_id> <bundle_id>
Android
adb shell dumpsys jobscheduler | grep <package_name>
adb shell cmd jobscheduler run -f <package_name> <job_id>
Common Issues
| Symptom | Cause | Fix |
|---|
| iOS: Tasks never execute | Missing com.transistorsoft.fetch in Info.plist | Add to BGTaskSchedulerPermittedIdentifiers |
| iOS: Tasks stop after a while | Not calling BackgroundFetch.finish() | Always call finish, even on timeout |
| iOS: Works in debug, not release | Missing @pragma('vm:entry-point') | Add pragma to both callback functions |
| Android: Tasks don't survive reboot | Missing RECEIVE_BOOT_COMPLETED | Add permission to AndroidManifest.xml |
| Android: Release build crash | R8 removing Worker classes | Add ProGuard keep rules |
| Both: SDK calls fail in background | Using MethodChannel SDK in isolate | Cache state in SharedPreferences, read in background |
| Both: Subscription check fails | Cache key mismatch foreground/background | Verify keyPrefix produces same SharedPreferences key |
Audit Checklist
When reviewing a Flutter background task setup, verify:
iOS
Android
Cross-Platform
Testing Strategy
Unit Tests
test('foreground/background cache key consistency', () async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final repo = AgentRepository(prefs, keyPrefix: 'myapp');
// Simulate foreground write
await prefs.setBool('myapp_subscription_cached', true);
// Background read should match
expect(repo.getCachedSubscriptionStatus(), true);
});
Integration Tests
- Build release APK/IPA
- Install on device
- Enable the background task
- Kill the app
- Wait for task to trigger (or force-trigger)
- Check logs/SharedPreferences for evidence of execution
iOS Simulator Testing
xcrun simctl background_fetch booted com.example.myapp
Related skills
flutter-verify โ use to verify background tasks are executing correctly on real devices and checking for runtime errors.