How to Implement a Reliable Flutter Background Service for Android and iOS
Implement a reliable Flutter background service by combining platform-specific native APIs—Android Foreground Services and iOS BGTaskScheduler—with Flutter MethodChannels, proper manifest configurations, and dedicated Dart isolates to ensure consistent execution across platforms.
Running code while a Flutter app is in the background requires platform-specific mechanisms, because Flutter’s Dart execution is paused when the app is not in the foreground. The most dependable approach is to combine a Flutter plugin that exposes native background APIs with the proper manifest and Info.plist configuration while respecting each platform’s lifecycle rules. This article references the flutter/flutter repository to demonstrate how to leverage MethodChannel infrastructure and embedding APIs for robust background execution.
Android Implementation: Foreground Services and MethodChannels
Android requires a foreground Service to keep your process alive when the app is not visible. This service must display a persistent notification and communicate with your Dart code through MethodChannel.
Creating a Foreground Service in Kotlin
In android/app/src/main/java/com/example/background/BackgroundService.kt, extend Service and call startForeground() within onCreate() to promote your service to foreground status:
package com.example.background
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.IBinder
import androidx.core.app.NotificationCompat
class BackgroundService : Service() {
private val handler = Handler()
private val runnable = object : Runnable {
override fun run() {
// Call into Flutter via MethodChannel
MethodChannelUtils.invokeDartMethod("onBackgroundTick")
handler.postDelayed(this, 15_000) // every 15 s
}
}
override fun onCreate() {
super.onCreate()
startForeground(NOTIF_ID, createNotification())
handler.post(runnable)
}
private fun createNotification(): Notification {
val channelId = "bg_service_channel"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Background Service",
NotificationManager.IMPORTANCE_LOW
)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
return NotificationCompat.Builder(this, channelId)
.setContentTitle("App running in background")
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
.build()
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(runnable)
}
override fun onBind(intent: Intent?): IBinder? = null
}
Communicating with Dart via MethodChannel
The MethodChannel class in packages/flutter/lib/services/platform_channel.dart provides the bridge between Kotlin and Dart. On the Dart side, create a channel with the same name used in native code:
import 'package:flutter/services.dart';
class AndroidBackground {
static const _channel = MethodChannel('com.example.background');
static Future<void> start() async {
await _channel.invokeMethod('startService');
}
static Future<void> stop() async {
await _channel.invokeMethod('stopService');
}
}
Handling Battery Optimizations
To ensure your service survives Doze mode and app standby, request the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission in your AndroidManifest.xml. You must also guide users to disable battery optimization for your app in system settings if continuous background execution is critical.
iOS Implementation: BGTaskScheduler and Background Modes
iOS restricts background execution strictly. You must use BGTaskScheduler (iOS 13+) or background modes like fetch and remote-notification, and you cannot guarantee indefinite execution like on Android.
Registering Background Tasks in Swift
In ios/Runner/BackgroundTaskDelegate.swift, register your task identifier with the system scheduler:
import BackgroundTasks
import Flutter
@objc class BackgroundTaskDelegate: NSObject {
static let shared = BackgroundTaskDelegate()
private let channel = FlutterMethodChannel(name: "com.example.background",
binaryMessenger: (UIApplication.shared.delegate as! FlutterAppDelegate).flutterEngine!.binaryMessenger)
func registerBackgroundTask() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.fetch",
using: nil) { task in
self.handleBackgroundFetch(task: task as! BGAppRefreshTask)
}
}
private func handleBackgroundFetch(task: BGAppRefreshTask) {
// Give us up to 30 seconds to run Dart code
let deadline = DispatchTime.now() + .seconds(30)
// Call into Dart
channel.invokeMethod("onBackgroundFetch", arguments: nil) { _ in
task.setTaskCompleted(success: true)
}
// Ensure completion even if Dart does not reply
DispatchQueue.global().asyncAfter(deadline: deadline) {
task.setTaskCompleted(success: false)
}
// Schedule next run
scheduleBackgroundFetch()
}
func scheduleBackgroundFetch() {
let request = BGAppRefreshTaskRequest(identifier: "com.example.fetch")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 min later
try? BGTaskScheduler.shared.submit(request)
}
}
Dart Integration for iOS
On the Dart side, initialize the iOS background task registration:
import 'package:flutter/services.dart';
import 'dart:io';
class IOSBackground {
static const _channel = MethodChannel('com.example.background');
static Future<void> initialize() async {
if (Platform.isIOS) {
await _channel.invokeMethod('registerTask');
}
}
}
Cross-Platform Architecture and Isolate Management
A production-ready flutter background service requires a unified Dart API that abstracts platform differences while leveraging IsolateNameServer or flutter_isolate to keep logic alive independently of the UI.
Unified Dart API
Create a platform-agnostic interface that delegates to the appropriate native implementation:
import 'dart:io';
import 'package:flutter/services.dart';
class BackgroundService {
static Future<void> start() async {
if (Platform.isAndroid) {
await AndroidBackground.start();
} else if (Platform.isIOS) {
await IOSBackground.initialize(); // registers the BGTaskScheduler
}
}
static Future<void> stop() async {
if (Platform.isAndroid) {
await AndroidBackground.stop();
}
// iOS tasks are automatically cancelled when completed; you may
// call a platform method to cancel a scheduled BGTask if needed.
}
}
Source Files and Platform Channel Infrastructure
The following files from the flutter/flutter repository define the infrastructure used by background service implementations:
packages/flutter/lib/services/platform_channel.dart– Contains theMethodChannelclass used to marshal calls between Dart and native code.engine/src/flutter/embedding/android/FlutterActivity.java– The entry point where Android services can be attached to the Flutter engine.engine/src/flutter/embedding/ios/FlutterViewController.mm– Where iOS background callbacks are forwarded to the Dart layer.dev/integration_tests/widget_preview_scaffold/lib/src/theme/ide_theme.dart– Demonstrates parsing platform channel arguments in Dart.dev/tools/create_api_docs.dart– Reference for official API documentation generation.
Summary
- Android requires a foreground Service with a persistent notification to prevent the system from killing your process, using
MethodChannelinpackages/flutter/lib/services/platform_channel.dartto bridge to Dart. - iOS uses BGTaskScheduler (iOS 13+) with specific background modes declared in
Info.plist, respecting strict time limits (typically 30 seconds) and scheduling tasks rather than running continuously. - Cross-platform implementations should expose a unified Dart API that delegates to platform-specific classes, using isolates to prevent UI jank and ensure state persistence.
- Battery optimization must be handled explicitly on Android via
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, while iOS requires adherence to system-granted execution windows.
Frequently Asked Questions
How do I keep a Flutter background service running when the app is closed?
On Android, you must implement a foreground Service that calls startForeground() with a persistent notification, as the system treats foreground services as user-visible work that should not be killed. On iOS, you cannot keep code running indefinitely when the app is closed; instead, you must use BGTaskScheduler to register periodic tasks that the system will execute at opportune times, typically when the device is charging or connected to Wi-Fi.
What is the difference between Android Foreground Services and iOS Background Tasks?
Android Foreground Services allow your app to run code continuously for an indefinite period, provided you display a persistent notification and handle battery optimization exemptions. In contrast, iOS Background Tasks (via BGTaskScheduler) are strictly time-limited (usually 30 seconds) and event-driven; the system decides when to grant execution time based on device conditions, and you cannot force continuous background execution on iOS.
Can I run Dart code continuously in the background on both platforms?
No, you cannot run Dart code continuously on both platforms using the same approach. On Android, you can maintain a long-running Dart isolate using flutter_isolate or IsolateNameServer alongside a foreground service, though the UI isolate will still be paused. On iOS, continuous execution is impossible; you must spawn a temporary isolate when BGTaskScheduler grants execution time, complete your work within the allotted window, and then terminate the isolate.
How do I handle communication between the background service and the main app?
Use MethodChannel defined in packages/flutter/lib/services/platform_channel.dart to establish bidirectional communication. On Android, the foreground service invokes methods on the channel to send data to the main isolate, while the Dart side can call native methods to start or stop the service. On iOS, the BGTaskScheduler callback uses the same channel to notify Dart when background execution begins, allowing you to pass completion handlers back to native code to signal task completion.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →