Background Download Not Working in release builds- flutter_downloader

Issue: Background downloads using the flutter_downloader package work perfectly in debug mode and release mode when run directly from Xcode (plugged in). However, when I create an archive build and install the app separately (via TestFlight or direct IPA install), the background download stops working as soon as the app is minimized.


✅ What I’ve already done

Info.plist

<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
    <string>fetch</string>
    <string>processing</string>
    <string>audio</string>
    <string>push-to-talk</string>
</array>

AppDelegate.swift

import UIKit
import Flutter
import Firebase
import flutter_downloader
import BackgroundTasks

@main
@objc class AppDelegate: FlutterAppDelegate {
    static let backgroundChannel = "com.example.app/background_service"
    private var backgroundCompletionHandler: (() -> Void)?

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        FirebaseApp.configure()
        GeneratedPluginRegistrant.register(with: self)
        FlutterDownloaderPlugin.setPluginRegistrantCallback(registerPlugins)

        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().delegate = self
        }

        if #available(iOS 13.0, *) {
            registerBackgroundTask()
        }

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    @available(iOS 13.0, *)
    private func registerBackgroundTask() {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: "com.example.app.process_download_queue",
            using: nil
        ) { [weak self] task in
            guard let self = self else { return }
            self.handleDownloadQueueTask(task: task as! BGProcessingTask)
        }
    }

    @available(iOS 13.0, *)
    private func handleDownloadQueueTask(task: BGProcessingTask) {
        scheduleNextDownloadTask()

        let headlessEngine = FlutterEngine(name: "BackgroundTaskEngine", project: nil, allowHeadlessExecution: true)
        headlessEngine.run()

        let channel = FlutterMethodChannel(
            name: AppDelegate.backgroundChannel,
            binaryMessenger: headlessEngine.binaryMessenger
        )

        task.expirationHandler = {
            channel.invokeMethod("backgroundTaskExpired", arguments: nil)
        }

        channel.invokeMethod("processNextInBackground", arguments: nil) { result in
            task.setTaskCompleted(success: (result as? Bool) ?? false)
        }
    }

    override func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void
    ) {
        self.backgroundCompletionHandler = completionHandler
        super.application(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler)
    }

    override func applicationDidEnterBackground(_ application: UIApplication) {
        if #available(iOS 13.0, *) {
            scheduleNextDownloadTask()
        }
    }

    @available(iOS 10.0, *)
    override func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        if #available(iOS 14.0, *) {
            completionHandler([.list, .banner, .badge, .sound])
        } else {
            completionHandler([.alert, .badge, .sound])
        }
    }

    @available(iOS 10.0, *)
    override func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        completionHandler()
    }
}

// MARK: - Helper
@available(iOS 13.0, *)
func scheduleNextDownloadTask() {
    let request = BGProcessingTaskRequest(identifier: "com.example.app.process_download_queue")
    request.requiresNetworkConnectivity = true
    request.requiresExternalPower = false
    request.earliestBeginDate = Date(timeIntervalSinceNow: 60)

    do {
        try BGTaskScheduler.shared.submit(request)
        print("BGTask: Download queue processing task scheduled successfully.")
    } catch {
        print("BGTask: Could not schedule download queue task: \(error)")
    }
}

private func registerPlugins(registry: FlutterPluginRegistry) {
    if !registry.hasPlugin("FlutterDownloaderPlugin") {
        FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
    }
}

🧩 Observations

  • Background download works correctly when:

    • The app is plugged in and run via Xcode (release/debug)
  • It stops working when:

    • The app is installed from an archived build (IPA/TestFlight) and minimized
  • All entitlements and background modes are properly added.

  • Provisioning profile includes required background modes.


❓Question

Is there any known limitation or signing difference between Xcode run and archived release builds that could cause URLSession background tasks not to trigger? Has anyone faced a similar issue when using flutter_downloader on iOS 13+ with BGTaskScheduler or URLSession background configuration?

Any help or working setup example for production/TestFlight would be appreciated.

Answered by DTS Engineer in 865373022

It’s hard for me to offer a definitive answer here because I’m not familiar with the third-party tools you’re using, but problems like this with background execution are pretty common. You seem to be combining two different background execution facilities:

  • Background fetch
  • URLSession background sessions

Both of these have behaviours that are known to confuse folks. For background fetch, see my explanation in iOS Background Execution Limits. For URLSession background sessions, see Testing Background Session Code.

Finally, I want to point you at a couple of other posts:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

It’s hard for me to offer a definitive answer here because I’m not familiar with the third-party tools you’re using, but problems like this with background execution are pretty common. You seem to be combining two different background execution facilities:

  • Background fetch
  • URLSession background sessions

Both of these have behaviours that are known to confuse folks. For background fetch, see my explanation in iOS Background Execution Limits. For URLSession background sessions, see Testing Background Session Code.

Finally, I want to point you at a couple of other posts:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Background Download Not Working in release builds- flutter_downloader
 
 
Q