# Platform Setup Guide Comprehensive guide for configuring KMP WorkManager on Android and iOS. ## Table of Contents - [Android Setup](#android-setup) - [iOS Setup](#ios-setup) - [Testing Background Tasks](#testing-background-tasks) - [Troubleshooting](#troubleshooting) --- ## Android Setup ### 1. Dependencies Add to your `build.gradle.kts`: ```kotlin kotlin { sourceSets { androidMain.dependencies { // KMP WorkManager (required) implementation("dev.brewkits:kmpworkmanager:1.1.0") // WorkManager (optional - already included transitively) implementation("androidx.work:work-runtime-ktx:2.11.0") // For Kotlin coroutines support implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") } } } ``` --- ### 2. AndroidManifest.xml Configuration #### Required Permissions ```xml ``` --- ### 3. Application Class Setup Create an Application class to initialize Koin: ```kotlin class KMPWorkManagerApp : Application() { override fun onCreate() { super.onCreate() startKoin { androidLogger(Level.DEBUG) androidContext(this@KMPWorkManagerApp) modules(kmpWorkerModule()) } // Optional: Configure WorkManager configureWorkManager() } private fun configureWorkManager() { val config = Configuration.Builder() .setMinimumLoggingLevel(Log.DEBUG) .setWorkerFactory(DefaultWorkerFactory()) .build() WorkManager.initialize(this, config) } } ``` Register in `AndroidManifest.xml`: ```xml ``` --- ### 4. Request Runtime Permissions (Android 13+) For Android 13+, request notification permission at runtime: ```kotlin class MainActivity : ComponentActivity() { private val notificationPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { println("Notification permission granted") } else { println("Notification permission denied") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { notificationPermissionLauncher.launch( Manifest.permission.POST_NOTIFICATIONS ) } // Request exact alarm permission for Android 12+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val alarmManager = getSystemService(AlarmManager::class.java) if (!alarmManager.canScheduleExactAlarms()) { Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).also { startActivity(it) } } } } } ``` --- ### 5. ProGuard Rules (if using R8/ProGuard) Add to `proguard-rules.pro`: ```proguard # Keep WorkManager classes -keep class androidx.work.** { *; } -keep class dev.brewkits.kmpworkmanager.sample.background.** { *; } # Keep Koin classes -keep class org.koin.** { *; } # Keep Kotlin coroutines -keepclassmembernames class kotlinx.** { *; } ``` --- ### 6. Worker Implementation Add your worker logic to `KmpWorker.kt`: ```kotlin class KmpWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val workerClassName = inputData.getString("workerClassName") ?: return Result.failure() val input = inputData.getString("input") Logger.i(LogTags.WORKER, "Executing worker: $workerClassName") return when (workerClassName) { "SyncWorker" -> executeSyncWorker(input) "UploadWorker" -> executeUploadWorker(input) // Add your workers here else -> { Logger.e(LogTags.WORKER, "Unknown worker: $workerClassName") Result.failure() } } } private suspend fun executeSyncWorker(input: String?): Result { return try { // Your sync logic here delay(2000) TaskEventBus.emit( TaskCompletionEvent("SyncWorker", true, "✅ Sync complete") ) Result.success() } catch (e: Exception) { Logger.e(LogTags.WORKER, "Sync failed", e) Result.retry() } } private suspend fun executeUploadWorker(input: String?): Result { return try { // Your upload logic here delay(3000) TaskEventBus.emit( TaskCompletionEvent("UploadWorker", true, "✅ Upload complete") ) Result.success() } catch (e: Exception) { Logger.e(LogTags.WORKER, "Upload failed", e) Result.retry() } } } ``` --- ### 7. Android-Specific Features #### Heavy Tasks (Foreground Service) For tasks longer than 10 minutes, set `isHeavyTask = true`: ```kotlin scheduler.enqueue( id = "ml-training", trigger = TaskTrigger.OneTime(), workerClassName = "MLTrainingWorker", constraints = Constraints( isHeavyTask = true, requiresCharging = true ) ) ``` This uses `KmpHeavyWorker` which runs as a foreground service. #### Expedited Work For high-priority tasks that need to run ASAP: ```kotlin scheduler.enqueue( id = "urgent-sync", trigger = TaskTrigger.OneTime(), workerClassName = "SyncWorker", constraints = Constraints( expedited = true ) ) ``` #### ContentUri Triggers Monitor MediaStore changes: ```kotlin scheduler.enqueue( id = "media-observer", trigger = TaskTrigger.ContentUri( uriString = "content://media/external/images/media", triggerForDescendants = true ), workerClassName = "MediaSyncWorker" ) ``` --- ## iOS Setup > [!WARNING] > **Critical iOS Limitations - Read Before Implementing** > > iOS background tasks are **fundamentally different** from Android: > > 1. **Opportunistic Execution**: The system decides when to run tasks. Tasks may be delayed hours or never run. > 2. **Strict Time Limits**: BGAppRefreshTask has ~30 seconds max, BGProcessingTask has ~60 seconds. > 3. **Force-Quit Termination**: All background tasks are **immediately killed** when user force-quits the app. > 4. **Limited Constraints**: iOS only supports network constraints. Charging, battery, and storage constraints are not available. > > **Do NOT use iOS background tasks for**: > - Time-critical operations > - Long-running processes (> 30s) > - Operations that must complete reliably > > See **[iOS Best Practices](ios-best-practices.md)** for detailed guidance and **[iOS Migration Guide](ios-migration.md)** for converting Android patterns to iOS. ### 1. Info.plist Configuration Add background task identifiers and capabilities: ```xml BGTaskSchedulerPermittedIdentifiers periodic-sync-task upload-task heavy-processing-task kmp_chain_executor_task UIBackgroundModes processing fetch remote-notification UIApplicationSceneManifest UIApplicationSupportsMultipleScenes ``` --- ### 2. Xcode Project Settings Open your Xcode project and verify: 1. **Signing & Capabilities**: - Add "Background Modes" capability - Enable "Background fetch" and "Background processing" 2. **Build Settings**: - Set `INFOPLIST_KEY_UIApplicationSceneManifest_Generation = NO` (if using AppDelegate) - Ensure deployment target is iOS 13.0 or higher 3. **General**: - Verify bundle identifier matches your configuration --- ### 3. AppDelegate Setup Create or update `iOSApp.swift`: ```swift import SwiftUI import BackgroundTasks import composeApp @main struct iOSApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Initialize Koin KoinIOSKt.doInitKoinIos() // Register background tasks registerBackgroundTasks() // Request notification permissions requestNotificationPermissions() return true } private func registerBackgroundTasks() { let koinIos = KoinIOS() // Periodic sync task (BGAppRefreshTask) BGTaskScheduler.shared.register( forTaskWithIdentifier: "periodic-sync-task", using: nil ) { task in self.handleAppRefreshTask( task: task as! BGAppRefreshTask, scheduler: koinIos.getScheduler() ) } // Heavy processing task (BGProcessingTask) BGTaskScheduler.shared.register( forTaskWithIdentifier: "heavy-processing-task", using: nil ) { task in self.handleProcessingTask( task: task as! BGProcessingTask, scheduler: koinIos.getScheduler() ) } // Chain executor task BGTaskScheduler.shared.register( forTaskWithIdentifier: "kmp_chain_executor_task", using: nil ) { task in self.handleChainExecutorTask( task: task as! BGProcessingTask, scheduler: koinIos.getScheduler() ) } } private func handleAppRefreshTask( task: BGAppRefreshTask, scheduler: BackgroundTaskScheduler ) { scheduler.handleSingleTask( task: task, taskIdentifier: "periodic-sync-task" ) } private func handleProcessingTask( task: BGProcessingTask, scheduler: BackgroundTaskScheduler ) { scheduler.handleSingleTask( task: task, taskIdentifier: "heavy-processing-task" ) } private func handleChainExecutorTask( task: BGProcessingTask, scheduler: BackgroundTaskScheduler ) { scheduler.handleChainExecutorTask(task: task) } private func requestNotificationPermissions() { UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .sound, .badge] ) { granted, error in if granted { print("Notification permission granted") } else if let error = error { print("Notification permission error: \(error)") } } } func applicationDidEnterBackground(_ application: UIApplication) { // Background tasks can now execute print("App entered background") } } ``` --- ### 4. Worker Implementation Create worker classes in `iosMain/background/workers/`: ```kotlin // SyncWorker.kt class SyncWorker : IosWorker { override suspend fun doWork(input: String?): Boolean { return try { // IMPORTANT: Must complete within 25 seconds for BGAppRefreshTask // or within a few minutes for BGProcessingTask Logger.i(LogTags.WORKER, "iOS SyncWorker started") delay(2000) // Simulate work TaskEventBus.emit( TaskCompletionEvent("SyncWorker", true, "✅ iOS sync complete") ) true } catch (e: Exception) { Logger.e(LogTags.WORKER, "iOS sync failed", e) false } } } ``` Register in `IosWorkerFactory.kt`: ```kotlin object IosWorkerFactory { fun createWorker(className: String): IosWorker? { return when (className) { "SyncWorker" -> SyncWorker() "UploadWorker" -> UploadWorker() "HeavyProcessingWorker" -> HeavyProcessingWorker() else -> { Logger.e(LogTags.FACTORY, "Unknown worker: $className") null } } } } ``` --- ### 5. iOS-Specific Features #### BGAppRefreshTask vs BGProcessingTask ```kotlin // Light task (BGAppRefreshTask - 25 seconds max) scheduler.enqueue( id = "quick-sync", trigger = TaskTrigger.Periodic(15_MINUTES), workerClassName = "SyncWorker", constraints = Constraints( isHeavyTask = false // Uses BGAppRefreshTask ) ) // Heavy task (BGProcessingTask - several minutes) scheduler.enqueue( id = "ml-training", trigger = TaskTrigger.OneTime(), workerClassName = "MLTrainingWorker", constraints = Constraints( isHeavyTask = true // Uses BGProcessingTask ) ) ``` #### Quality of Service (QoS) Control task priority on iOS: ```kotlin scheduler.enqueue( id = "high-priority-sync", trigger = TaskTrigger.Periodic(15_MINUTES), workerClassName = "SyncWorker", constraints = Constraints( qos = QualityOfService.HIGH // User-initiated priority ) ) ``` #### Silent Push Notifications For remote-triggered tasks, configure APNS: 1. Enable "Remote notifications" in Background Modes 2. Send silent push with `content-available: 1` 3. Handle in `didReceiveRemoteNotification`: ```swift func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { let koinIos = KoinIOS() let scheduler = koinIos.getScheduler() // Trigger background task Task { await scheduler.enqueue( id: "push-triggered-sync", trigger: TaskTriggerOneTime(initialDelayMs: 0), workerClassName: "SyncWorker", input: nil, constraints: Constraints() ) completionHandler(.newData) } } ``` --- ## Testing Background Tasks ### Android Testing #### 1. Force Run WorkManager Task ```bash # View all scheduled tasks adb shell dumpsys jobscheduler | grep KmpWorker # Force run a task (requires WorkManager Test helpers) adb shell am broadcast -a androidx.work.diagnostics.REQUEST_DIAGNOSTICS # Check WorkManager database adb shell sqlite3 /data/data/YOUR.PACKAGE.NAME/databases/androidx.work.workdatabase "SELECT * FROM WorkSpec" ``` #### 2. Test Doze Mode ```bash # Unplug device adb shell dumpsys battery unplug # Enter Doze mode adb shell dumpsys deviceidle force-idle # Exit Doze mode adb shell dumpsys deviceidle unforce # Reset battery adb shell dumpsys battery reset ``` #### 3. Test Exact Alarms ```bash # Check if app can schedule exact alarms adb shell dumpsys alarm | grep YOUR.PACKAGE.NAME # View next alarm adb shell dumpsys alarm | grep -A 20 "Next alarm clock" ``` --- ### iOS Testing #### 1. Simulator Testing with LLDB In Xcode, run the app and pause at a breakpoint, then in LLDB console: ```lldb # Force execute a BGAppRefreshTask e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"periodic-sync-task"] # Force execute a BGProcessingTask e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"heavy-processing-task"] ``` #### 2. Scheme Arguments Add launch arguments in Xcode scheme: 1. Product → Scheme → Edit Scheme 2. Run → Arguments → Arguments Passed On Launch 3. Add: `-BGTaskSchedulerSimulateEarlyTermination` This simulates app termination during background task execution. #### 3. Monitor Console Logs ```bash # View iOS device logs xcrun simctl spawn booted log stream --predicate 'subsystem == "com.apple.BGTaskScheduler"' --level debug # Or use Console.app to filter by BGTaskScheduler ``` #### 4. Physical Device Testing 1. **Connect device to Xcode** 2. **Run app and send to background** (Home button) 3. **Wait or trigger via LLDB** (connect debugger to running app) 4. **Check logs in Xcode Console** **Important**: BGTasks only run on physical devices when app is truly in background and system decides to execute them. Testing requires patience or LLDB simulation. --- ## Troubleshooting ### Android Issues #### Tasks Not Running **Problem**: Scheduled tasks never execute **Solutions**: 1. Check WorkManager initialization: ```kotlin val workManager = WorkManager.getInstance(context) val workInfos = workManager.getWorkInfosForUniqueWork("task-id").get() println("Work state: ${workInfos.firstOrNull()?.state}") ``` 2. Verify constraints are met: ```bash adb shell dumpsys battery unplug adb shell svc wifi enable ``` 3. Check for Doze mode restrictions: ```bash adb shell dumpsys battery unplug adb shell dumpsys deviceidle whitelist +YOUR.PACKAGE.NAME ``` --- #### Exact Alarms Not Triggering **Problem**: `TaskTrigger.Exact` doesn't fire **Solutions**: 1. Check permission: ```kotlin val alarmManager = getSystemService(AlarmManager::class.java) val canSchedule = alarmManager.canScheduleExactAlarms() println("Can schedule exact alarms: $canSchedule") ``` 2. Request permission manually: ```kotlin val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM) startActivity(intent) ``` 3. Verify `AlarmReceiver` is registered in `AndroidManifest.xml` --- #### Foreground Service Crashes **Problem**: `KmpHeavyWorker` crashes with `ForegroundServiceStartNotAllowedException` **Solutions**: 1. Add foreground service permission: ```xml ``` 2. Request notification permission on Android 13+ 3. Create proper notification channel: ```kotlin val channel = NotificationChannel( "heavy_task_channel", "Heavy Tasks", NotificationManager.IMPORTANCE_LOW ) notificationManager.createNotificationChannel(channel) ``` --- ### iOS Issues #### Background Tasks Not Executing **Problem**: BGTasks never run on device **Solutions**: 1. **Verify Info.plist configuration**: - Check `BGTaskSchedulerPermittedIdentifiers` array - Ensure task IDs match exactly (case-sensitive) 2. **Check AppDelegate registration**: ```swift BGTaskScheduler.shared.register( forTaskWithIdentifier: "periodic-sync-task", using: nil ) { task in // Handler must be registered BEFORE task is scheduled } ``` 3. **App must be in background**: - Press Home button to background app - Wait several minutes or hours (iOS decides when to run) - Use LLDB to force execution for testing 4. **Check system logs**: ```bash log stream --predicate 'subsystem == "com.apple.BGTaskScheduler"' --level debug ``` --- #### Tasks Timeout After 25 Seconds **Problem**: BGAppRefreshTask terminates after 25 seconds **Solutions**: 1. **Use BGProcessingTask for longer work**: ```kotlin constraints = Constraints(isHeavyTask = true) ``` 2. **Optimize worker to complete faster**: ```kotlin class SyncWorker : IosWorker { override suspend fun doWork(input: String?): Boolean { withTimeout(20_000) { // Complete within 20 seconds // Fast sync logic } return true } } ``` 3. **Split work into chains**: ```kotlin scheduler .beginWith(TaskRequest(workerClassName = "QuickSync1")) .then(TaskRequest(workerClassName = "QuickSync2")) .enqueue() ``` --- #### Worker Not Found **Problem**: `IosWorkerFactory` returns null **Solutions**: 1. **Register worker in factory**: ```kotlin object IosWorkerFactory { fun createWorker(className: String): IosWorker? { return when (className) { "SyncWorker" -> SyncWorker() else -> null } } } ``` 2. **Check worker class name spelling** (case-sensitive) 3. **Verify worker implements `IosWorker` interface** --- #### Periodic Tasks Not Re-scheduling **Problem**: Task runs once but doesn't repeat **Solution**: Ensure `handleSingleTask` re-schedules periodic tasks: ```kotlin // This is handled internally by NativeTaskScheduler // Verify metadata is stored correctly val defaults = NSUserDefaults.standardUserDefaults val metadata = defaults.stringForKey("kmp_periodic_meta_periodic-sync-task") println("Periodic metadata: $metadata") ``` --- ## Best Practices ### Android 1. **Use `expedited = true` for urgent tasks** (< 10 minutes) 2. **Use `isHeavyTask = true` for long tasks** (> 10 minutes) 3. **Always handle `Result.retry()` for transient failures** 4. **Request permissions before scheduling tasks** 5. **Test in Doze mode** to ensure tasks run when expected --- ### iOS 1. **Keep BGAppRefreshTask workers under 20 seconds** 2. **Use BGProcessingTask (`isHeavyTask = true`) for heavy work** 3. **Always re-schedule periodic tasks after completion** 4. **Register task handlers BEFORE scheduling tasks** 5. **Test on physical devices** (simulator behavior differs) 6. **Use LLDB commands for testing** (don't wait hours for iOS to trigger) 7. **Store task metadata in UserDefaults** for persistence --- ## Next Steps - [Quick Start Guide](quickstart.md) - Get started quickly - [API Reference](api-reference.md) - Complete API documentation - [Task Chains](task-chains.md) - Build complex workflows - [Constraints & Triggers](constraints-triggers.md) - All trigger types --- Need help? [Open an issue](https://github.com/brewkits/kmp_worker/issues) or ask in [Discussions](https://github.com/brewkits/kmp_worker/discussions).