Skip to content

Latest commit

 

History

History
677 lines (511 loc) · 20.3 KB

File metadata and controls

677 lines (511 loc) · 20.3 KB

Android Setup Guide

This guide covers Android-specific configuration for native_workmanager.


Prerequisites

  • Android Studio Arctic Fox (2020.3.1) or later
  • Kotlin 1.9.0+
  • Gradle 7.0+
  • Flutter SDK 3.0+

Minimum Requirements

1. Minimum SDK Version

The plugin requires Android API 26 (Android 8.0) as the minimum SDK version.

Edit android/app/build.gradle:

android {
    compileSdk 34

    defaultConfig {
        applicationId "com.example.yourapp"
        minSdk 26  // ⚠️ REQUIRED: Must be 26 or higher
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = "17"
    }
}

Why API 26?

  • Android WorkManager requires API 23+ for basic functionality
  • Native workers use advanced features requiring API 26+
  • Ensures consistent behavior across Android versions

2. Android 14+ (API 34) Compatibility — Foreground Services

Starting with Android 14 (API 34), all Foreground Services must declare a foregroundServiceType.

As of v1.4.5, native_workmanager does NOT declare FOREGROUND_SERVICE / FOREGROUND_SERVICE_DATA_SYNC or the SystemForegroundService type override in its own manifest anymore. Earlier versions did this unconditionally, which merged those permissions into every consumer app regardless of whether it used foreground services — Google Play flags apps that carry foreground-service permissions they never actually use, so plain httpDownload/fileMkdir/etc.-only apps were getting rejected for something they didn't need.

If your app uses isHeavyTask: true (or KmpHeavyWorker/ForegroundNativeWorker directly), add these to your OWN android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

    <application>
        <!-- Override WorkManager's SystemForegroundService to declare the type(s)
             your app actually uses. "dataSync" covers most background-sync use cases;
             see doc/API_REFERENCE.md's ForegroundServiceType enum for the full list
             (location, mediaPlayback, camera, microphone, health, etc.) — each
             non-dataSync type needs its own matching permission too, e.g.
             android.permission.FOREGROUND_SERVICE_LOCATION for `location`. -->
        <service
            android:name="androidx.work.impl.foreground.SystemForegroundService"
            android:foregroundServiceType="dataSync"
            tools:node="merge" />
    </application>
</manifest>

Then specify the matching foregroundServiceType in your Constraints:

constraints: Constraints(
  isHeavyTask: true,
  foregroundServiceType: ForegroundServiceType.location,
  foregroundNotificationConfig: ForegroundNotificationConfig(
    title: "Tracking Location",
    body: "GPS is active in background",
  ),
)

If you never use isHeavyTask, you need none of the above — the plugin works entirely without foreground-service permissions for standard enqueue/beginWith tasks.

Without the manifest entries, setForeground() throws SecurityException on Android 14+ — the plugin catches and logs this internally rather than crashing, so the task still runs, just as regular (non-prioritized) background work instead of a foreground service. See example/android/app/src/main/AndroidManifest.xml for a working reference.


Installation

1. Add Dependency

Add to your pubspec.yaml:

dependencies:
  native_workmanager: ^1.5.0

Run:

flutter pub get

2. Basic Setup

For native workers (HTTP, file, crypto, image, PDF, etc.) that run while the app is in the foreground or background (but not yet killed), no extra Android configuration is needed.

However, if you want any task (Native or Dart) to trigger reliably after the app process has been killed (by the OS or the user), you must follow the Killed-App Support instructions in the next section.


3. Killed-App Support

When Android kills your app (low memory, user swipe) and WorkManager later fires a scheduled task, the process restarts. For the task to succeed, WorkManager must be initialized with the plugin's custom WorkerFactory.

Since v1.3.0 this is handled automatically. The plugin ships an androidx.startup Initializer that runs before Application.onCreate(). No manual setup is required for the common case.

Default (v1.3.0+) — Zero Configuration ✅

Nothing to do. The plugin's manifest merge installs NativeWorkManagerInitializer automatically. It reads the persisted callbackHandle, restores security settings (enforceHttps, blockPrivateIPs), and calls KmpWorkManager.initialize() with the plugin's built-in worker factory before any pending task fires.

Opt-out — If your app has a custom WorkManager configuration (e.g., you implement Configuration.Provider), add this to your <application> block to disable auto-init:

<meta-data
    android:name="native_workmanager.auto_init"
    android:value="false" />

Then follow the Manual Setup steps below.


Manual Setup (advanced — only when opting out of auto-init)

Step 1 — Create (or update) your Application class

// android/app/src/main/kotlin/com/example/myapp/MyApplication.kt
package com.example.myapp

import android.content.Context
import androidx.work.Configuration
import androidx.work.DelegatingWorkerFactory
import dev.brewkits.kmpworkmanager.background.KmpWorkerFactory
import dev.brewkits.native_workmanager.SimpleAndroidWorkerFactory
import dev.brewkits.native_workmanager.engine.FlutterEngineManager
import io.flutter.app.FlutterApplication

class MyApplication : FlutterApplication(), Configuration.Provider {

    override fun onCreate() {
        super.onCreate()

        // Restore callbackHandle that the plugin persisted during Dart-side initialize().
        // SharedPreferences name and key mirror the plugin's internal constants.
        val handle = getSharedPreferences(
            "dev.brewkits.native_workmanager", Context.MODE_PRIVATE
        ).getLong("callback_handle", -1L)

        if (handle != -1L) {
            FlutterEngineManager.setCallbackHandle(handle)
        }
    }

    // WorkManager calls this when the process is restarted after being killed,
    // before any Flutter engine or plugin is loaded.
    // It is NOT called during a normal app launch (plugin already initialized WorkManager first).
    override fun getWorkManagerConfiguration(): Configuration {
        val factory = DelegatingWorkerFactory().apply {
            addFactory(KmpWorkerFactory(SimpleAndroidWorkerFactory(this@MyApplication)))
        }
        return Configuration.Builder()
            .setWorkerFactory(factory)
            .build()
    }
}

If you also register custom native workers, pass your user factory to SimpleAndroidWorkerFactory:

addFactory(KmpWorkerFactory(SimpleAndroidWorkerFactory(this, myUserFactory)))

ProGuard / R8 note: The plugin automatically ships ProGuard rules that protect its own classes (via consumerProguardFiles). However, your custom worker classes live in your app's package and are not covered. Add a keep rule for them in android/app/proguard-rules.pro:

-keep class com.example.yourapp.workers.** { *; }

Without this, R8 will rename the class in Release builds and WorkManager will fail to instantiate it (the class name is stored as a String in SQLite and resolved via reflection).

Step 2 — Register the Application class in AndroidManifest.xml

<application
    android:name=".MyApplication"
    ...>

Step 3 — Disable WorkManager's default auto-initializer

WorkManager ships a ContentProvider-based initializer that runs before Application.onCreate() and initializes WorkManager with the default (no custom factory) configuration. If it fires first, your Configuration.Provider is ignored and KmpWorker creation fails.

Remove it in android/app/src/main/AndroidManifest.xml:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <!-- Remove the default WorkManager initializer so Configuration.Provider is used instead -->
    <meta-data
        android:name="androidx.work.WorkManagerInitializer"
        android:value="androidx.startup"
        tools:node="remove" />
</provider>

Add the tools namespace to the <manifest> tag if not already present:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

How the two startup paths interact

Normal launch:
  Application.onCreate() → restore callbackHandle only
  Flutter plugin onAttachedToEngine() → KmpWorkManager.initialize() → WorkManager initialized ✅
  Configuration.Provider.getWorkManagerConfiguration() → NOT called (already initialized)

Killed-app restart (WorkManager fires a task):
  Application.onCreate() → restore callbackHandle ✅
  WorkManager not yet initialized → calls getWorkManagerConfiguration() ✅
  KmpWorkerFactory creates KmpWorker → DartCallbackWorker runs
  FlutterEngineManager uses restored callbackHandle to boot Dart engine ✅

Battery optimisation (OS constraint)

On most Android devices users must exempt your app from battery optimisation for WorkManager to run tasks reliably after the app is killed. This is an OS constraint, not a plugin limitation.

Prompt the user from your settings screen:

import 'package:flutter/services.dart';

// Android-only — check and request battery optimisation exemption
const _channel = MethodChannel('your_app/battery');
await _channel.invokeMethod('requestIgnoreBatteryOptimizations');

On the Kotlin side:

val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
    data = Uri.parse("package:${packageName}")
}
startActivity(intent)

Initialization

Basic Initialization

import 'package:flutter/material.dart';
import 'package:native_workmanager/native_workmanager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize before runApp()
  await NativeWorkManager.initialize();

  runApp(MyApp());
}

Advanced Initialization (with Dart Workers)

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await NativeWorkManager.initialize(
    dartWorkers: {
      'processData': _processDataCallback,
      'syncDatabase': _syncDatabaseCallback,
    },
    // When using other plugins (like flutter_local_notifications) in the background
    registerPlugins: true,
    // Security options
    enforceHttps: true,
    blockPrivateIPs: true,
    // Maintenance
    cleanupAfterDays: 30,
  );

  runApp(MyApp());
}

// Must be top-level or static — NOT a closure or instance method
@pragma('vm:entry-point')
Future<bool> _processDataCallback(Map<String, dynamic>? input) async {
  // Your Dart logic here
  return true;
}

@pragma('vm:entry-point')
Future<bool> _syncDatabaseCallback(Map<String, dynamic>? input) async {
  // Database sync logic
  return true;
}

Custom Native Workers

If you need to register custom native workers:

1. Create your worker:

// android/app/src/main/kotlin/com/example/yourapp/workers/AnalyticsFlushWorker.kt
package com.example.yourapp.workers

import dev.brewkits.kmpworkmanager.background.domain.AndroidWorker
import dev.brewkits.kmpworkmanager.background.domain.AndroidWorkerResult

class AnalyticsFlushWorker : AndroidWorker {
    override suspend fun doWork(inputJson: String?): AndroidWorkerResult {
        // flush analytics
        return AndroidWorkerResult.success()
    }
}

2. Register the factory — two patterns depending on your setup:

Without killed-app support (no custom Application):

// In MainActivity.kt, before super.onCreate()
SimpleAndroidWorkerFactory.setUserFactory { workerClassName ->
    when (workerClassName) {
        "AnalyticsFlushWorker" -> AnalyticsFlushWorker()
        else -> null
    }
}

With killed-app support (custom Application from §3):

Pass the factory in getWorkManagerConfiguration() and also set it at launch:

// MyApplication.kt
private val userFactory = AndroidWorkerFactory { workerClassName ->
    when (workerClassName) {
        "AnalyticsFlushWorker" -> AnalyticsFlushWorker()
        else -> null
    }
}

override fun onCreate() {
    super.onCreate()
    // ... restore callbackHandle ...
    // Make the factory available before Flutter loads
    SimpleAndroidWorkerFactory.setUserFactory(userFactory)
}

override fun getWorkManagerConfiguration(): Configuration {
    val factory = DelegatingWorkerFactory().apply {
        addFactory(KmpWorkerFactory(SimpleAndroidWorkerFactory(this@MyApplication, userFactory)))
    }
    return Configuration.Builder().setWorkerFactory(factory).build()
}

3. Use in Dart:

await NativeWorkManager.enqueue(
  taskId: 'flush-analytics',
  trigger: TaskTrigger.oneTime(),
  worker: NativeWorker.custom(
    workerClassName: 'AnalyticsFlushWorker',
    input: {'batchSize': 100},
  ),
);

See full custom workers guide →


Verification

Check Logcat

After scheduling a task, check Android Logcat:

adb logcat -s NativeWorkmanagerPlugin,FlutterEngineManager,DartCallbackWorker

Expected on first launch:

NativeWorkmanagerPlugin: ✅ Scheduler initialized with kmpworkmanager
NativeWorkmanagerPlugin: callbackHandle persisted for cold-start: 12345678

Expected on killed-app restart:

MyApplication: Restored callbackHandle from prefs: 12345678
FlutterEngineManager: Initializing Flutter engine... (cold process start)
FlutterEngineManager: Dart ready signal received

Force-Run a Task (Debug)

# List scheduled jobs
adb shell dumpsys jobscheduler | grep -A 20 "your.package.name"

# Force-run the next pending job
adb shell cmd jobscheduler run -f your.package.name 1

Troubleshooting

DartWorker fails after app kill

Symptoms: Native workers run fine; DartWorker silently fails after process death.

Checklist:

  1. Did you add the custom Application class? (§3, Step 1)
  2. Is the Application class registered in AndroidManifest.xml? (§3, Step 2)
  3. Did you remove the default WorkManager initializer? (§3, Step 3)
  4. Is battery optimisation disabled for your app during testing?
  5. Check Logcat for getWorkManagerConfiguration() called — if missing, Step 3 is incomplete.

Other plugins (notifications, etc.) not working in DartWorker

Symptoms: Your DartWorker runs, but other plugins like flutter_local_notifications or shared_preferences don't seem to work or throw errors.

Solution: Enable plugin registration during initialization:

await NativeWorkManager.initialize(
  registerPlugins: true,
  dartWorkers: { ... },
);

By default, the background engine does not register plugins to save RAM and avoid side-effects (like disconnecting Bluetooth).

Selective Plugin Registration (Recommended)

To maintain peak performance and avoid side-effects (like Bluetooth drops), we recommend keeping registerPlugins: false and manually registering only the necessary plugins for your background tasks.

In your MainActivity.kt (or MainApplication.kt):

import dev.brewkits.native_workmanager.NativeWorkmanagerPlugin
import io.flutter.embedding.engine.FlutterEngine
import com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        NativeWorkmanagerPlugin.setPluginRegistrantCallback(object : NativeWorkmanagerPlugin.Companion.PluginRegistrantCallback {
            override fun registerWith(engine: FlutterEngine) {
                // Register ONLY the plugins needed for your background workers
                engine.plugins.add(FlutterLocalNotificationsPlugin())
            }
        })
    }
}

Error: "KmpWorkManager not initialized"

Cause: NativeWorkManager.initialize() was not called or failed.

Solution:

// ❌ Wrong — missing await
void main() {
  NativeWorkManager.initialize();  // Not awaited!
  runApp(MyApp());
}

// ✅ Correct
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await NativeWorkManager.initialize();
  runApp(MyApp());
}

Error: "Unresolved reference: kmpworkmanager"

flutter clean
cd android && ./gradlew clean && cd ..
flutter pub get
flutter build apk --debug

Error: "Minimum SDK version is X but should be 26"

Edit android/app/build.gradle:

defaultConfig {
    minSdk 26
}

Error: "WorkManager already initialized" / crash on hot-restart

This was a known v1.0.7 bug (H-2). Fixed by resetting isSchedulerInitialized = false in onDetachedFromEngine. Ensure you are on v1.0.7 or later.


Tasks not running in background

  1. Battery optimisation — request exemption (see §3 above)
  2. Doze mode — use Constraints(requiresNetwork: true) to let WorkManager reschedule
  3. App standby — use periodic intervals ≥ 15 minutes
  4. Simulate Doze mode:
    adb shell dumpsys battery unplug
    adb shell dumpsys deviceidle force-idle

High memory usage

Use native workers for I/O — they don't spin up a Flutter engine:

// ❌ Dart worker — ~50 MB RAM, 1–2 s cold start
DartWorker(callbackId: 'httpRequest')

// ✅ Native worker — ~2–5 MB RAM, <50 ms cold start
NativeWorker.httpRequest(url: '...')

ProGuard / R8 Configuration

# native_workmanager
-keep class dev.brewkits.native_workmanager.** { *; }
-keep class dev.brewkits.kmpworkmanager.** { *; }

# Keep WorkManager worker classes
-keep class * extends androidx.work.Worker { *; }
-keep class * extends androidx.work.ListenableWorker { *; }
-keep class * implements dev.brewkits.kmpworkmanager.background.domain.AndroidWorker { *; }

# WorkManager
-keep class androidx.work.** { *; }

Production Checklist

  • minSdk is 26 or higher
  • Tested on Android 8, 10, 12, 14+
  • If using DartWorker: killed-app support active (auto-init default in v1.3.0+, or manual Configuration.Provider if opted out)
  • If opted out of auto-init (native_workmanager.auto_init=false): custom Application + WorkManager default initializer removed
  • Battery optimisation exemption UI implemented and tested
  • Tested: tasks run after app force-close (with battery opt disabled)
  • Tested: tasks run after device reboot
  • Tested: task chains with mid-chain failure
  • Memory profiled (Android Profiler) — no engine leak after autoDispose
  • ProGuard/R8 tested if using obfuscation
  • debugMode: false (or omitted) in release builds

Platform-Specific Behaviour

Doze Mode (Android 6.0+)

Tasks can be deferred during Doze. WorkManager handles rescheduling automatically. For time-sensitive work use Constraints(requiresCharging: false) and accept eventual execution.

App Standby (Android 6.0+)

Inactive apps get background-access buckets (Active → Working set → Frequent → Rare → Restricted). Use intervals ≥ 15 minutes and constrained tasks to stay in higher buckets.

Exact Alarms (Android 12+)

TaskTrigger.exact() requires SCHEDULE_EXACT_ALARM permission on Android 12+. WorkManager falls back to an inexact trigger if the permission is not granted (ScheduleResult.rejectedOsPolicy). Check the result:

final handler = await NativeWorkManager.enqueue(...);
if (handler.scheduleResult == ScheduleResult.rejectedOsPolicy) {
  // prompt user to grant exact alarm permission
}

Next Steps


Last Updated: May 2026 (v1.2.7)