react-native-background-downloader

kesha-antonov活跃维护

为 React Native 应用提供 iOS 和 Android 上的大文件后台下载与上传能力,即使应用退到后台或被系统终止,传输仍能继续完成。

Download and upload large files on iOS & Android — even when your app is in the background or terminated.

v4.8.0发布于 2026-11-06 20:17 UTC225star +1 / 7 天51fork同步于 2026-11-11 00:02 UTC

怎么上手

由 README 提炼,执行前请确认
yarn add @kesha-antonov/react-native-background-downloader
硬件要求按 README 说明

README 未说明硬件要求,仅标注支持 iOS 与 Android 平台

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

React Native 后台下载上传库,让大文件在 App 退到后台或被系统终止后仍继续传输

为 React Native 应用提供 iOS 和 Android 上的大文件后台下载与上传能力,即使应用退到后台或被系统终止,传输仍能继续完成。

核心能力

  • 基于 iOS NSURLSession 在独立进程继续下载
  • Android 用 DownloadManager 加前台服务实现暂停恢复
  • App 重启后用 getExistingDownloadTasks 重新接回任务
  • 支持后台文件上传与自定义请求头鉴权
  • 提供 Expo 配置插件与 TurboModules 新架构支持

容易踩的坑

用户从 App Switcher 手动强杀 App 时 iOS 会取消所有后台任务,属系统限制无法绕过;Android 端依赖 MMKV 做持久化状态存储;Expo 项目需额外启用配置插件

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v4.8.0
  1. v4.8.04.8.0最新正式版2026-11-06 · 2026-11-06 20:17 UTC
  2. v4.7.04.7.02026-10-31 · 2026-10-31 20:41 UTC
    查看更新说明
  3. v4.6.44.6.42026-10-21 · 2026-10-21 11:30 UTC
    查看更新说明
  4. v4.6.34.6.32026-09-15 · 2026-09-15 15:33 UTC
    查看更新说明
  5. v4.6.24.6.22026-09-05 · 2026-09-05 11:37 UTC
    查看更新说明

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    Support my work

    cryptoc - my crypto portfolio app. Your coins on the home screen, lock screen and watch face. iPhone, iPad, Mac, Apple Watch, Android, Android tablet and Wear OS.

    • Portfolio with average buy price and 24h / 180-day / all-time P&L
    • Widgets in three sizes, refreshed in the background - most days you never open the app
    • Price alerts on 5,000+ coins, delivered while the app is closed
    • No account, no email, no exchange API keys, no ads. Your holdings never reach a server - they sync through your own iCloud or Google Drive
    • Free for 3 holdings, and that is not a trial timer

    Downloading it is what pays for the time that goes into these libraries.

    ✨ Features

    • 📥 Background Downloads - Downloads continue even when app is in background or terminated by the OS
    • 📤 Background Uploads - Upload files reliably in the background
    • ⏸️ Pause/Resume - Full pause and resume support on both iOS and Android
    • 🔄 Re-attach to Downloads - Reconnect to ongoing downloads after app restart
    • 📊 Progress Tracking - Real-time progress updates with customizable intervals
    • 🔒 Custom Headers - Support for authentication and custom request headers
    • 📱 Expo Support - Config plugin for easy Expo integration
    • ⚡ New Architecture - Full TurboModules support for React Native
    • 📝 TypeScript - Complete TypeScript definitions included

    💡 Why?

    The Problem: Standard network requests in React Native are tied to your app's lifecycle. When the user switches to another app or the OS terminates your app to free memory, your downloads stop. For small files this is fine, but for large files (videos, podcasts, documents) this creates a frustrating user experience.

    The Solution: Both iOS and Android provide system-level APIs for background file transfers:

    • iOS: NSURLSession - handles downloads in a separate process, continuing even after your app is terminated by the OS. Note: if the user explicitly force-kills the app via the App Switcher, iOS cancels all background tasks — this is an iOS system limitation that cannot be overridden
    • Android: A combination of DownloadManager for system-managed downloads, Foreground Services for pause/resume support, and MMKV for persistent state storage

    The Challenge: These APIs are powerful but complex. Downloads run in a separate process, so your app might restart from scratch while downloads are still in progress. Keeping your UI in sync with background downloads requires careful state management.

    This Library: @kesha-antonov/react-native-background-downloader wraps these native APIs in a simple, unified JavaScript interface. Start a download, close your app, reopen it hours later, and seamlessly reconnect to your ongoing downloads with a single function call.

    ⚖️ Comparison

    How this library compares to other file transfer options in the React Native ecosystem:

    CapabilityThis libraryexpo-file-systemreact-native-blob-utilreact-native-fs
    Background downloads on iOS✅✅ 1❌✅ 4
    Background downloads on Android✅❌ 2⚠️ 3❌
    Background uploads✅ iOS + Android⚠️ iOS only❌❌
    Reconnect to transfers after app restart✅❌ 2❌❌
    Pause / resume✅ iOS + Android⚠️ iOS only❌⚠️ iOS only 5
    Progress events✅✅✅✅
    Expo config plugin✅built in❌❌
    New Architecture (TurboModules)✅✅✅⚠️ via community fork
    General filesystem API❌✅✅✅

    1 Via sessionType: 'background' (iOS only).

    2 From the Expo docs: "Android accepts this option for API consistency and ignores it", and "the JavaScript DownloadTask instance is not restored if the app is terminated or relaunched, so its promise, progress callbacks, and cancellation state are only available while the original JS runtime is still alive."

    3 Via useDownloadManager: true, which is GET-only and can only write to external storage.

    4 Via the background: true option (iOS only).

    5 Via the resumable callback (iOS only).

    The key difference is the highlighted row. Several libraries can hand a transfer to the OS. This is the one that can find those transfers again after your app has been killed and relaunched - getExistingDownloadTasks() returns live task objects you can re-bind your UI to, on both platforms.

    When you probably don't need this library: if your files are small, or a failed transfer can simply be restarted from zero next time the user opens the app, expo-file-system is simpler and already in your project. Reach for this one when a transfer is long enough that losing it costs the user real time.

    📖 Table of Contents

    📋 Requirements

    RequirementVersion
    React Native>= 0.70.0
    iOS>= 15.1
    AndroidAPI 24+ (Android 7.0)
    ExpoSDK 50+ (with config plugin)

    Note: For older React Native versions (0.57.0 - 0.69.x), use version 2.x of this library.

    📦 Installation

    Expo Projects

    Step 1: Install the package

    npx expo install @kesha-antonov/react-native-background-downloader
    

    Step 2: Add the config plugin to your app.json or app.config.js:

    {
      "expo": {
        "plugins": [
          "@kesha-antonov/react-native-background-downloader"
        ]
      }
    }
    
    // app.config.js
    export default {
      expo: {
        plugins: [
          ["@kesha-antonov/react-native-background-downloader", {
            mmkvVersion: "1.3.16",  // Customize MMKV version on Android
            skipMmkvDependency: true  // Skip if you want to add MMKV manually
          }]
        ]
      }
    }
    
    OptionTypeDefaultDescription
    mmkvVersionstring'1.3.16'The version of MMKV to use on Android. See MMKV version comparison for details.
    skipMmkvDependencybooleanfalseSkip adding MMKV dependency. Set to true if you're using react-native-mmkv to avoid duplicate class errors. The plugin auto-detects react-native-mmkv but you can use this option to explicitly skip. See MMKV version comparison.

    Step 3: Rebuild your app

    npx expo prebuild --clean
    npx expo run:ios   # or npx expo run:android
    

    The plugin automatically handles:

    • iOS: Adding the required handleEventsForBackgroundURLSession method to AppDelegate
    • Android: Adding the required MMKV dependency

    Bare React Native Projects

    Step 1: Install the package

    yarn:

    yarn add @kesha-antonov/react-native-background-downloader
    

    npm:

    npm install @kesha-antonov/react-native-background-downloader
    

    Step 2: Install iOS pods

    yarn:

    cd ios && pod install && cd ..
    

    npm:

    cd ios && pod install && cd ..
    

    Step 3: Configure iOS AppDelegate

    In your project bridging header file (e.g. ios/{projectName}-Bridging-Header.h):

    #import <RNBackgroundDownloader.h>
    

    In your AppDelegate.swift:

    func application(
      _ application: UIApplication,
      handleEventsForBackgroundURLSession identifier: String,
      completionHandler: @escaping () -> Void
    ) {
      RNBackgroundDownloader.setCompletionHandlerWithIdentifier(identifier, completionHandler: completionHandler)
    }
    

    In your AppDelegate.m:

    #import <RNBackgroundDownloader.h>
    
    - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
    {
      [RNBackgroundDownloader setCompletionHandlerWithIdentifier:identifier completionHandler:completionHandler];
    }
    

    Step 4: Configure Android MMKV dependency

    Add MMKV to your android/app/build.gradle:

    dependencies {
        implementation 'com.tencent:mmkv-shared:1.3.16'
    }
    

    Note: If you're already using react-native-mmkv in your project, skip this step — it already includes MMKV. Note that react-native-mmkv v4.x uses Margelo's fork of MMKV (io.github.zhongwuzw:mmkv) which re-adds armeabi-v7a (32-bit ARM) support that was dropped in the official MMKV 2.x release.

    ⚠️ armeabi-v7a (32-bit ARM) users: MMKV 2.x dropped 32-bit ABI support (since v2.0.0). If you need armeabi-v7a support and get a CMake error like No compatible library found for //mmkv/mmkv, use the MMKV 1.3.x LTS series instead — it supports both armeabi-v7a and 16KB page sizes (since v1.3.14):

    dependencies {
        implementation 'com.tencent:mmkv-shared:1.3.16'
    }
    
    MMKV version comparison
    Dependencyarmeabi-v7a (32-bit)arm64-v8a16KB page sizeRecommended for
    com.tencent:mmkv-shared:1.3.16 (default)✅✅✅ (since 1.3.14)Most apps — broadest device coverage
    com.tencent:mmkv-shared:2.x❌✅✅64-bit only apps (no legacy devices)
    io.github.zhongwuzw:mmkv:2.3.0 (Margelo fork)✅✅✅Used automatically by react-native-mmkv v4.x — skip manual dependency
    react-native-mmkv (already in project)✅✅✅If you already use react-native-mmkv — skip Step 4 entirely

    TL;DR: Use the default 1.3.16. If you already have react-native-mmkv in your project, skip Step 4.

    🚀 Usage

    Downloading a file

    import { Platform } from 'react-native'
    import { createDownloadTask, completeHandler, directories } from '@kesha-antonov/react-native-background-downloader'
    
    const jobId = 'file123'
    
    let task = createDownloadTask({
      id: jobId,
      url: 'https://link-to-very.large/file.zip',
      destination: `${directories.documents}/file.zip`,
      metadata: {}
    }).begin(({ expectedBytes, headers }) => {
      console.log(`Going to download ${expectedBytes} bytes!`)
    }).progress(({ bytesDownloaded, bytesTotal }) => {
      console.log(`Downloaded: ${bytesDownloaded / bytesTotal * 100}%`)
    }).done(({ bytesDownloaded, bytesTotal }) => {
      console.log('Download is done!', { bytesDownloaded, bytesTotal })
    
      // PROCESS YOUR STUFF
    
      // FINISH DOWNLOAD JOB
      completeHandler(jobId)
    }).error(({ error, errorCode }) => {
      console.log('Download canceled due to error: ', { error, errorCode });
    
      completeHandler(jobId)
    })
    
    // starts download
    task.start()
    
    // ...later
    
    // Pause the task
    await task.pause()
    
    // Resume after pause
    await task.resume()
    
    // Cancel the task
    await task.stop()
    

    Re-Attaching to background tasks

    The killer feature of this library: reconnect to downloads and uploads that continued running while your app was closed, or resume paused tasks from a previous session.

    When the OS terminates your app to free memory, background transfers keep running. When your app restarts, call getExistingDownloadTasks() or getExistingUploadTasks() to get back in sync. Paused tasks are also preserved and can be resumed with task.resume().

    💡 Tip: Use meaningful task IDs (not random UUIDs) so you can match tasks to your UI components after restart.

    Downloads:

    import { getExistingDownloadTasks } from '@kesha-antonov/react-native-background-downloader'
    
    const lostTasks = await getExistingDownloadTasks()
    
    for (const task of lostTasks) {
      console.log(`Found download: ${task.id}`)
    
      task.progress(({ bytesDownloaded, bytesTotal }) => {
        console.log(`Downloaded: ${bytesDownloaded / bytesTotal * 100}%`)
      }).done(({ location, bytesDownloaded, bytesTotal }) => {
        console.log('Download complete!', { location, bytesDownloaded, bytesTotal })
      }).error(({ error, errorCode }) => {
        console.log('Download failed:', { error, errorCode })
      })
    }
    

    Uploads:

    import { getExistingUploadTasks } from '@kesha-antonov/react-native-background-downloader'
    
    const lostUploads = await getExistingUploadTasks()
    
    for (const task of lostUploads) {
      console.log(`Found upload: ${task.id}`)
    
      task.progress(({ bytesUploaded, bytesTotal }) => {
        console.log(`Uploaded: ${bytesUploaded / bytesTotal * 100}%`)
      }).done(({ responseCode, responseBody }) => {
        console.log('Upload complete!', { responseCode, responseBody })
      }).error(({ error, errorCode }) => {
        console.log('Upload failed:', { error, errorCode })
      })
    }
    
    import { Platform } from 'react-native'
    import { createUploadTask, completeHandler, directories } from '@kesha-antonov/react-native-background-downloader'
    
    const jobId = 'upload123'
    
    let task = createUploadTask({
      id: jobId,
      url: 'https://your-server.com/upload',
      source: `${directories.documents}/photo.jpg`,
      method: 'POST', // or 'PUT', 'PATCH'
      fieldName: 'file', // multipart form field name
      mimeType: 'image/jpeg',
      parameters: {
        userId: '123',
        description: 'My photo'
      },
      metadata: {}
    }).begin(({ expectedBytes }) => {
      console.log(`Going to upload ${expectedBytes} bytes!`)
    }).progress(({ bytesUploaded, bytesTotal }) => {
      console.log(`Uploaded: ${bytesUploaded / bytesTotal * 100}%`)
    }).done(({ responseCode, responseBody, bytesUploaded, bytesTotal }) => {
      console.log('Upload is done!', { responseCode, responseBody })
    
      // PROCESS YOUR STUFF
    
      // FINISH UPLOAD JOB
      completeHandler(jobId)
    }).error(({ error, errorCode }) => {
      console.log('Upload canceled due to error: ', { error, errorCode })
    
      completeHandler(jobId)
    })
    
    // starts upload
    task.start()
    
    // ...later
    
    // Pause the task (platform support may vary)
    await task.pause()
    
    // Resume after pause
    await task.resume()
    
    // Cancel the task
    await task.stop()
    

    Pass one presigned URL per part as partUrls: the library sends each byte range of the file to its URL under one task, with combined progress, and hands back every part's ETag for CompleteMultipartUpload. No part files are created on Android; on iOS each part is copied to a temporary file while it uploads.

    import { createUploadTask, directories } from '@kesha-antonov/react-native-background-downloader'
    
    // From your backend: CreateMultipartUpload, then a presigned UploadPart URL per part
    const { uploadId, partUrls, partSize } = await api.startMultipartUpload({ fileSize })
    
    const task = createUploadTask({
      id: 'video-upload',
      source: `${directories.documents}/video.mp4`,
      partUrls, // method defaults to PUT
      partSize, // optional: defaults to Math.ceil(fileSize / partUrls.length)
      mimeType: 'video/mp4',
      metadata: { uploadId },
    }).progress(({ bytesUploaded, bytesTotal }) => {
      console.log(`Uploaded: ${bytesUploaded / bytesTotal * 100}%`)
    }).done(async ({ parts }) => {
      await api.completeMultipartUpload({
        uploadId,
        parts: parts.map(({ partNumber, responseHeaders }) => ({ PartNumber: partNumber, ETag: responseHeaders.etag })),
      })
    })
    
    task.start()
    

    A single upload's response headers are in done too, as responseHeaders (lowercased names). On iOS, an upload that finishes while the app isn't running comes back from getExistingUploadTasks() as a DONE task with parts and responseHeaders set. See Uploading in parts.

    If your download uses authentication tokens that expire, you can update the headers of a paused download before resuming it. This is useful when auth tokens refresh while a download is paused:

    import { getExistingDownloadTasks } from '@kesha-antonov/react-native-background-downloader'
    
    // Get paused downloads
    const tasks = await getExistingDownloadTasks()
    
    for (const task of tasks) {
      if (task.state === 'PAUSED') {
        // Update headers with new auth token before resuming
        await task.setDownloadParams({
          ...task.downloadParams,
          headers: {
            ...task.downloadParams?.headers,
            Authorization: 'Bearer new-refreshed-token'
          }
        })
    
        // Now resume with the updated headers
        await task.resume()
      }
    }
    

    Notes:

    • setDownloadParams() is async and returns true if native headers were updated
    • Headers are only updated in the native layer when the task is in PAUSED state
    • On iOS, the download will resume using HTTP Range headers with the new headers
    • On Android, both in-memory and persisted paused state are updated

    Use case: User pauses a large download, closes the app, and returns hours or days later. By then, the auth token has expired. This feature allows refreshing the token and updating headers before resuming, without restarting the download from scratch.

    ⚙️ Advanced Configuration

    1. Globally using setConfig():
    import { setConfig } from '@kesha-antonov/react-native-background-downloader'
    
    setConfig({
      headers: {
        Authorization: 'Bearer 2we$@$@Ddd223',
      }
    })
    

    This way, all downloads with have the given headers.

    1. Per download by passing a headers object in the options of createDownloadTask():
    import { createDownloadTask, directories } from '@kesha-antonov/react-native-background-downloader'
    
    const task = createDownloadTask({
      id: 'file123',
      url: 'https://link-to-very.large/file.zip'
      destination: `${directories.documents}/file.zip`,
      headers: {
        Authorization: 'Bearer 2we$@$@Ddd223'
      }
    }).begin(({ expectedBytes, headers }) => {
      console.log(`Going to download ${expectedBytes} bytes!`)
    }).progress(({ bytesDownloaded, bytesTotal }) => {
      console.log(`Downloaded: ${bytesDownloaded / bytesTotal * 100}%`)
    }).done(({ location, bytesDownloaded, bytesTotal }) => {
      console.log('Download is done!', { location, bytesDownloaded, bytesTotal })
    }).error(({ error, errorCode }) => {
      console.log('Download canceled due to error: ', { error, errorCode })
    })
    
    task.start()
    

    Headers given in createDownloadTask() are merged with the ones given in setConfig({ headers: { ... } }).

    You can configure global settings for download behavior using setConfig():

    Max Parallel Downloads

    Control how many downloads transfer at once. This is useful for managing bandwidth and server load.

    import { setConfig } from '@kesha-antonov/react-native-background-downloader'
    
    // Set maximum parallel downloads to 8 (default is 4)
    setConfig({
      maxParallelDownloads: 8
    })
    

    Note: the two platforms apply it at different layers. On iOS it is the download session's maximum simultaneous connections per host. On Android it caps the library's own downloader - the mechanism used on Android 16+, and whenever DownloadManager or a UIDT job can't take the download - while downloads that run through DownloadManager or the JobScheduler are queued by those schedulers instead.

    Cellular/WiFi Restrictions

    Control whether downloads are allowed over cellular (metered) networks:

    import { setConfig } from '@kesha-antonov/react-native-background-downloader'
    
    // Only allow downloads over WiFi (disable cellular data)
    setConfig({
      allowsCellularAccess: false
    })
    
    // Allow downloads over both WiFi and cellular (default)
    setConfig({
      allowsCellularAccess: true
    })
    

    This is a cross-platform setting that works on both iOS and Android, for downloads and uploads. It's the default for each task's isAllowedOverMetered, so it applies to tasks created after the call:

    • iOS: Sets allowsCellularAccess on each task's request (the session itself always allows cellular)
    • Android: Applied on all three download mechanisms - DownloadManager requests (setAllowedOverMetered), the UIDT/JobScheduler jobs used on Android 14+ (the job requires an unmetered network), and the foreground-service fallback used on Android < 14 (gated on a ConnectivityManager unmetered-network callback)

    When cellular is disallowed and the device only has a metered connection, the download doesn't fail - it waits until an unmetered network (e.g. WiFi) becomes available, then starts automatically. If the unmetered network is lost mid-download (e.g. WiFi drops and the device falls back to cellular), the download pauses instead of failing and automatically resumes from where it left off once an unmetered network returns. The restriction is kept across pause/resume and app restarts.

    Per-task override: You can override the global cellular setting for individual tasks using the isAllowedOverMetered option in createDownloadTask() or createUploadTask(), in either direction:

    const task = createDownloadTask({
      id: 'file123',
      url: 'https://link-to-very.large/file.zip',
      destination: `${directories.documents}/file.zip`,
      isAllowedOverMetered: true  // This download can use cellular even if global setting is false
    })
    

    On iOS, files protected with NSFileProtectionComplete cannot be written while the device is locked. Since a background download can finish while the screen is locked, the library saves files with completeUntilFirstUserAuthentication by default - writable while locked after the first unlock since boot - and, if a save still can't happen because the device is locked, it stages the bytes and finishes the save (emitting complete) when the device is next unlocked.

    You usually don't need to change this. If your app has stricter security requirements you can raise the protection level globally or per task (iOS only - ignored on Android):

    import { setConfig, createDownloadTask, directories } from '@kesha-antonov/react-native-background-downloader'
    
    // Global default for all downloads
    setConfig({
      iosDataProtection: 'completeUntilFirstUserAuthentication', // default
    })
    
    // Per-download override
    const task = createDownloadTask({
      id: 'secret-doc',
      url: 'https://example.com/secret.pdf',
      destination: `${directories.documents}/secret.pdf`,
      iosDataProtection: 'complete', // strongest; note: won't be writable while locked before first unlock
    })
    
    ValueNSFileProtection levelNotes
    'completeUntilFirstUserAuthentication'NSFileProtectionCompleteUntilFirstUserAuthenticationDefault. Accessible after the first unlock since boot - recommended for background downloads
    'complete'NSFileProtectionCompleteStrongest. File is inaccessible whenever the device is locked
    'completeUnlessOpen'NSFileProtectionCompleteUnlessOpenAccessible while open, even if the device locks afterward
    'none'NSFileProtectionNoneNo protection

    The library includes verbose debug logging that can help diagnose download issues. Logging is disabled by default but can be enabled at runtime using setConfig(). Logging works in both debug and production/release builds.

    import { setConfig } from '@kesha-antonov/react-native-background-downloader'
    
    // Option 1: Enable native console logging (logs appear in Xcode/Android Studio console)
    setConfig({
      isLogsEnabled: true
    })
    
    // Option 2: Enable logging with a JavaScript callback to capture logs in your app
    setConfig({
      isLogsEnabled: true,
      logCallback: (log) => {
        // log.message - The debug message
        // log.taskId - Optional task ID associated with the log (iOS only)
        console.log('[BackgroundDownloader]', log.message)
    
        // You can also send logs to your analytics/crash reporting service
        // crashlytics.log(log.message)
      }
    })
    
    // Disable logging
    setConfig({
      isLogsEnabled: false
    })
    

    Notes:

    • When isLogsEnabled is true, native debug logs (NSLog on iOS, Log.d/w/e on Android) are printed
    • The logCallback function is called for each native debug log (iOS only sends logs to callback currently)
    • Logs include detailed information about download lifecycle, session management, and errors
    • In production builds, logs are only printed when explicitly enabled via isLogsEnabled
    • Logs include URLs but not header values or upload response bodies; see Security Notes before forwarding them anywhere

    This library automatically includes connection timeout improvements for slow-responding URLs. By default, the following headers are added to all download requests on Android:

    • Connection: keep-alive - Keeps the connection open for better handling
    • Keep-Alive: timeout=600, max=1000 - Sets a 10-minute keep-alive timeout
    • User-Agent: ReactNative-BackgroundDownloader/3.2.6 - Proper user agent for better server compatibility

    These headers help prevent downloads from getting stuck in "pending" state when servers take several minutes to respond initially. You can override these headers by providing your own in the headers option.

    Android's DownloadManager has a built-in redirect limit that can cause ERROR_TOO_MANY_REDIRECTS for URLs with multiple redirects (common with podcast URLs, tracking services, CDNs, etc.).

    To handle this, you can use the maxRedirects option to pre-resolve redirects before passing the final URL to DownloadManager:

    import { Platform } from 'react-native'
    import { createDownloadTask, directories } from '@kesha-antonov/react-native-background-downloader'
    
    // Example: Podcast URL with multiple redirects
    const task = createDownloadTask({
      id: 'podcast-episode',
      url: 'https://pdst.fm/e/chrt.fm/track/479722/arttrk.com/p/example.mp3',
      destination: `${directories.documents}/episode.mp3`,
      maxRedirects: 10, // Follow up to 10 redirects before downloading
    }).begin(({ expectedBytes }) => {
      console.log(`Going to download ${expectedBytes} bytes!`)
    }).progress(({ bytesDownloaded, bytesTotal }) => {
      console.log(`Downloaded: ${bytesDownloaded / bytesTotal * 100}%`)
    }).done(({ location, bytesDownloaded, bytesTotal }) => {
      console.log('Download is done!', { location, bytesDownloaded, bytesTotal })
    }).error(({ error, errorCode }) => {
      console.log('Download canceled due to error: ', { error, errorCode })
    
      if (errorCode === 1005) { // ERROR_TOO_MANY_REDIRECTS
        console.log('Consider increasing maxRedirects or using a different URL')
      }
    })
    
    task.start()
    

    Notes on maxRedirects:

    • Only available on Android (iOS handles redirects automatically)
    • If not specified or set to 0, no redirect resolution is performed
    • Uses HEAD requests to resolve redirects efficiently
    • Falls back to original URL if redirect resolution fails
    • Respects the same headers and timeouts as the main download
    • While Authorization, Cookie or Proxy-Authorization is set, stops before a redirect to another origin, so the credentials aren't sent there; never follows a redirect to a non-http(s) URL or from https to http

    On Android 14+ (API 34), downloads use User-Initiated Data Transfer (UIDT) jobs which require notifications. Due to Android system requirements, notifications cannot be completely disabled when using UIDT jobs. However, you can control their visibility:

    • When showNotificationsEnabled: true - Full notifications with progress, title, and custom texts
    • When showNotificationsEnabled: false (default) - Minimal silent notifications with lowest priority that are barely noticeable

    Basic configuration:

    import { setConfig } from '@kesha-antonov/react-native-background-downloader'
    
    // Enable notifications and notification grouping with custom texts
    setConfig({
      showNotificationsEnabled: true, // Show full notifications (default: false - minimal silent notifications)
      notificationsGrouping: {
        enabled: true,           // Enable grouping (default: false)
        texts: {
          downloadTitle: 'Download',
          downloadStarting: 'Starting download...',
          downloadProgress: 'Downloading... {progress}%',
          downloadPaused: 'Paused',
          downloadFinished: 'Download complete',
          groupTitle: 'Downloads',
          groupText: '{count} downloads in progress',
        },
      },
    })
    
    // Use minimal silent notifications (default behavior)
    setConfig({
      showNotificationsEnabled: false, // Minimal silent notifications (required by UIDT but barely visible)
    })
    

    Cancel button and completion notification (Android 14+):

    Two extras are available on top of showNotificationsEnabled. Both are off by default, so enabling notifications alone never adds an alerting notification or a button your app is not ready for:

    setConfig({
      showNotificationsEnabled: true,
      // Adds a Cancel button to the download notification. Tapping it stops the
      // download like task.stop() and fires the task's .error() handler with
      // errorCode = -1, so handle that in your app before enabling it.
      showCancelAction: true,
      // Posts a "download complete" notification when a download finishes.
      // Tapping it opens the saved file with the system chooser.
      // Skipped in 'summaryOnly' grouping mode.
      showCompletionNotification: true,
      notificationsGrouping: {
        enabled: true,
        texts: {
          downloadCancel: 'Cancel',        // Label of the Cancel button
          downloadFinished: 'Download complete', // Title of the completion notification
        },
      },
    })
    

    Per-download notification titles are supported too - pass metadata.notificationTitle to override groupName and the default downloadTitle for a single download. See Platform Notes for the full behavior.

    Notification grouping modes:

    When downloading many files (e.g., thousands of photos), you can use the mode option to control how notifications are displayed:

    ModeDescription
    'individual'Default. Shows all individual notifications grouped together with a summary
    'summaryOnly'Shows only ONE notification with real-time aggregate progress (e.g., "45% - 5 files"). Individual UIDT notifications are collapsed into an invisible group. Ideal for bulk downloads
    import { setConfig } from '@kesha-antonov/react-native-background-downloader'
    
    // For bulk downloads (e.g., syncing thousands of photos)
    // Use 'summaryOnly' mode to show only ONE notification with aggregate progress
    setConfig({
      showNotificationsEnabled: true,
      notificationsGrouping: {
        enabled: true,
        mode: 'summaryOnly', // Only show summary notification with progress bar
        texts: {
          groupTitle: 'Syncing Photos',
          groupText: '{count} files downloading',
        },
      },
    })
    

    Example: Batch downloading multiple files with grouped notifications:

    import { setConfig, createDownloadTask, directories } from '@kesha-antonov/react-native-background-downloader'
    
    // Configure for bulk downloads with single summary notification
    setConfig({
      showNotificationsEnabled: true,
      notificationsGrouping: {
        enabled: true,
        mode: 'summaryOnly',
        texts: {
          groupTitle: 'Photo Sync',
          groupText: '{count} photos downloading',
        },
      },
    })
    
    // Download multiple files - they all share ONE notification with aggregate progress
    const photos = [
      { id: 'photo-1', url: 'https://example.com/photo1.jpg' },
      { id: 'photo-2', url: 'https://example.com/photo2.jpg' },
      { id: 'photo-3', url: 'https://example.com/photo3.jpg' },
      // ... potentially thousands of files
    ]
    
    const GROUP_ID = 'photo-sync-batch'
    
    for (const photo of photos) {
      const task = createDownloadTask({
        id: photo.id,
        url: photo.url,
        destination: `${directories.documents}/${photo.id}.jpg`,
        metadata: {
          groupId: GROUP_ID,        // Required for grouping
          groupName: 'Photo Sync',  // Displayed in notification title
        },
      })
        .progress(({ bytesDownloaded, bytesTotal }) => {
          // Progress tracked per file, notification shows aggregate progress
        })
        .done(() => {
          console.log(`Downloaded ${photo.id}`)
        })
        .error(({ error }) => {
          console.error(`Failed ${photo.id}:`, error)
        })
    
      task.start()
    }
    
    // User sees: ONE notification showing "45% - 3 files" with progress bar
    // instead of 3 separate notifications cluttering the notification shade
    // Notification automatically disappears when all downloads complete
    

    Grouping downloads by category:

    When notification grouping is enabled, you can group related downloads (e.g., by album, playlist, podcast) by passing groupId and groupName in the task metadata:

    import { createDownloadTask, directories } from '@kesha-antonov/react-native-background-downloader'
    
    // Download album songs - they will be grouped under one notification
    const task = createDownloadTask({
      id: 'song-1',
      url: 'https://example.com/albums/summer-hits/track01.mp3',
      destination: `${directories.documents}/track01.mp3`,
      metadata: {
        groupId: 'album-summer-hits',  // Unique identifier for the group
        groupName: 'Summer Hits 2024', // Display name in notification title
      },
    })
    
    task.start()
    

    Notification behavior during pause/resume:

    When a download is paused on Android 14+:

    • The background UIDT job is cancelled (to prevent downloads continuing in background)
    • A detached "Paused" notification remains visible showing current progress
    • When resumed, a new UIDT job is created and the notification switches to "Downloading" state
    • When stopped, the notification is removed
    • When app is closed, all download notifications are automatically removed

    This ensures users always see the download status without unexpected background activity.

    Configuration options:

    OptionTypeDefaultDescription
    showNotificationsEnabledbooleanfalseShow full download notifications. When false, creates minimal silent notifications (UIDT jobs require a notification, but it will be barely visible). This is a top-level config option.
    notificationsGrouping.enabledbooleanfalseEnable notification grouping
    notificationsGrouping.mode'individual' | 'summaryOnly''individual'Notification display mode. Use 'summaryOnly' for bulk downloads to show only ONE notification with real-time aggregate progress
    notificationsGrouping.textsobjectSee belowCustomizable notification texts

    Notification texts (notificationsGrouping.texts):

    KeyDefaultPlaceholdersDescription
    downloadTitle'Download'—Title for individual download notifications
    downloadStarting'Starting download...'—Text when download is starting
    downloadProgress'Downloading... {progress}%'{progress}Progress text with current percentage (0-100)
    downloadPaused'Paused'—Text when download is paused
    downloadFinished'Download complete'—Text when download is finished
    groupTitle'Downloads'—Title for group summary notification
    groupText'{count} download(s) in progress'{count}Group summary text with active downloads count

    Notes:

    • Notifications cannot be completely disabled on Android 14+ due to UIDT requirements
    • When showNotificationsEnabled: false, notifications are created with minimal visibility (lowest priority, empty content)
    • Paused downloads show a non-ongoing notification (can be swiped away by user)
    • Active downloads show an ongoing notification (cannot be swiped away)
    • This feature only affects Android 14+ (API 34) where UIDT jobs are used
    • On older Android versions, the standard DownloadManager notifications are shown
    • iOS uses system download notifications and doesn't support custom grouping
    • Use mode: 'summaryOnly' when downloading many files to prevent notification spam - shows ONE notification with aggregate progress bar that updates in real-time

    📚 API

    For complete API documentation, see the API Reference.

    Quick Reference

    import {
      setConfig,
      createDownloadTask,
      createUploadTask,
      getExistingDownloadTasks,
      getExistingUploadTasks,
      completeHandler,
      directories
    } from '@kesha-antonov/react-native-background-downloader'
    
    FunctionDescription
    createDownloadTask(options)Create a new download task
    createUploadTask(options)Create a new upload task
    getExistingDownloadTasks()Get downloads running in background
    getExistingUploadTasks()Get uploads running in background
    setConfig(config)Set global configuration
    completeHandler(jobId)Signal download completion to OS
    directories.documentsPath to app's documents directory

    📱 Platform Notes

    For detailed platform-specific information, see Platform Notes.

    Key points:

    • iOS: Uses NSURLSession for true background downloads
    • Android: Uses DownloadManager + Foreground Services + MMKV
    • Pause/Resume: Works on both platforms (Android requires server Range header support)

    🧪 Example App

    The repository includes a full example app demonstrating all features:

    cd example
    yarn install
    
    # iOS
    cd ios && pod install && cd ..
    yarn ios
    
    # Android
    yarn android
    

    The example app shows:

    • Starting multiple downloads
    • Pause/resume functionality
    • Progress tracking with animations
    • Re-attaching to background tasks
    • File management
    • Uploads to a test server
    • Light and dark theme, following the system setting

    ❓ Troubleshooting

    This can happen with slow-responding servers. The library automatically adds keep-alive headers, but you can also try:

    • Increase timeout by setting custom headers
    • Check if the server supports the download URL
    • Enable debug logs to see what's happening: setConfig({ isLogsEnabled: true })

    If you're using react-native-mmkv, you don't need to add the MMKV dependency manually - it's already included. The library uses compileOnly to avoid conflicts.

    react-native-mmkv v4.x uses Margelo's fork of MMKV (io.github.zhongwuzw:mmkv) which re-adds armeabi-v7a (32-bit ARM) support, so you have full ABI coverage including 32-bit devices when using react-native-mmkv.

    This was fixed in v4.4.0. Update to the latest version. The podspec declares the MMKV dependency itself, so you don't need to add anything to your Podfile. If you do pin it manually, exclude the broken 2.4.1 release (see the entry below):

    pod 'MMKV', '>= 1.0.0', '!= 2.4.1'
    
    Pods/MMKVCore/Core/aes/AESCrypt.cpp:83:11
    (void)memset_s(ptr, len, 0, len);
          ^ use of undeclared identifier 'memset_s'
    

    This is an upstream bug in MMKVCore 2.4.1 (Tencent/MMKV#1675) - it does not compile on Apple platforms. Update to the latest version of this library: the podspec now excludes exactly that release, so a fresh pod install resolves MMKVCore 2.4.0 instead.

    If you are on a version with the fix and still hitting this, your Podfile.lock is pinning the broken version. Refresh it:

    cd ios
    pod update MMKV MMKVCore
    

    If some other pod in your graph forces MMKVCore 2.4.1 and you cannot move off it, define the missing feature-test macro on the command line (the #define inside AESCrypt.cpp lands too late because CocoaPods force-includes the generated prefix header first):

    # ios/Podfile
    post_install do |installer|
      installer.pods_project.targets.each do |target|
        next unless target.name == 'MMKVCore'
        target.build_configurations.each do |config|
          config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
          config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '__STDC_WANT_LIB_EXT1__=1'
        end
      end
    end
    

    On iOS the actual transfer is handed to the system nsurlsessiond daemon and keeps running while the app is suspended and the screen is locked - this library already configures the background session correctly (discretionary = NO, sessionSendsLaunchEvents = YES). If it looks like downloads stop when locked, check these in order:

    1. Did you force-quit the app? If the user swipes the app away in the app switcher, iOS halts that app's background transfers until it's relaunched. This is an OS policy and cannot be worked around by any library.
    2. You won't get live progress events while locked. Your JavaScript isn't running while the app is suspended, so begin / progress / complete callbacks fire when the app resumes or is relaunched - not in real time. Re-attach with getExistingDownloadTasks() on launch.
    3. AppDelegate wiring. Make sure handleEventsForBackgroundURLSession is implemented (see Installation) and that you call completeHandler(jobId) from JS in your done and error handlers, for every task. After a background launch the library tells iOS it's finished once every task that finished in it has been passed to completeHandler (or after 25 seconds), so it doesn't get suspended while you're still processing them.
    4. Data Protection (the one this library handles). On a passcoded, locked device, files protected with NSFileProtectionComplete can't be written, so a download that finishes while locked could previously fail to save. The library now writes the file with NSFileProtectionCompleteUntilFirstUserAuthentication by default (writable while locked after the first unlock since boot), and if the move still can't happen because the device is locked it stages the bytes and completes the save automatically when the device is next unlocked - emitting complete then. You can change the level with setConfig({ iosDataProtection }) or per task (see Advanced Configuration).
    5. Test on a real device. The iOS Simulator's background-transfer behavior is unreliable; verify on hardware.

    Make sure to call getExistingDownloadTasks() at app startup and re-attach your callbacks. The task IDs you provide are used to identify downloads across restarts.

    See the Google Play Console Declaration section for the required steps.

    Add the Proguard rules mentioned in the Proguard Rules section.

    💡 Use Cases

    This library is perfect for apps that need reliable file transfers:

    • 🎵 Music/Podcast Apps - Download episodes for offline listening
    • 📚 E-book Readers - Download books in the background
    • 🎬 Video Streaming - Offline video downloads
    • 📁 File Managers - Large file transfers
    • 🎮 Games - Download game assets and updates
    • 📱 Enterprise Apps - Sync large documents and media

    🔄 Migration Guide

    Upgrading from an older version? Check the Migration Guide for detailed instructions:

    See the Changelog for a complete list of changes in each version.

    🤝 Contributing

    Contributions are welcome! Please feel free to submit a Pull Request.

    1. Fork the repository
    2. Create your feature branch (git checkout -b feature/amazing-feature)
    3. Install dependencies (yarn install)
    4. Make your changes
    5. Run tests (yarn test)
    6. Run linting (yarn lint)
    7. Commit your changes (git commit -m 'Add amazing feature')
    8. Push to the branch (git push origin feature/amazing-feature)
    9. Open a Pull Request

    Development Setup

    # Install dependencies
    yarn install
    
    # Run tests
    yarn test
    
    # Run linting
    yarn lint
    
    # Build the Expo plugin
    yarn build-plugin
    
    # Run the example app
    cd example && yarn install
    yarn ios  # or yarn android
    

    👥 Authors

    Maintained by Kesha Antonov

    Based on react-native-background-downloader by Elad Gil (unmaintained since 2019)

    I also build cryptoc - a crypto portfolio app with home screen, lock screen and Watch widgets, no account and no exchange API keys.

    Please note that this project is maintained in free time. If you find it helpful, please consider becoming a sponsor.

    📄 License

    Apache 2.0