Let's get KMP WorkManager running in your project. Should take about 5 minutes.
Add KMP WorkManager to your build.gradle.kts (module level):
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.brewkits:kmpworkmanager:2.4.3")
}
}
}Sync your project with Gradle files.
Add these permissions to your AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required for scheduling exact alarms (Android 12+) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- Required for notifications (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Required for heavy tasks using foreground services -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application>
<!-- Your app content -->
</application>
</manifest>Create or update your Application class:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(kmpWorkerModule())
}
}
}Update your AndroidManifest.xml to reference the Application class:
<application
android:name=".MyApp"
...>
</application>KMP WorkManager uses WorkManager internally, but you may want to add it explicitly:
androidMain.dependencies {
implementation("androidx.work:work-runtime-ktx:2.11.0")
}Add background task identifiers to your Info.plist:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>periodic-sync-task</string>
<string>upload-task</string>
<string>heavy-processing-task</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
<string>fetch</string>
<string>remote-notification</string>
</array>Create or update your iOSApp.swift:
import SwiftUI
import composeApp
@main
struct iOSApp: App {
init() {
// Initialize Koin
KoinIOSKt.doInitKoinIos()
// Register background tasks
registerBackgroundTasks()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
private func registerBackgroundTasks() {
let scheduler = koinIos.getScheduler()
let executor = koinIos.getSingleTaskExecutor()
let chainExecutor = koinIos.getChainExecutor()
// Register periodic sync task
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "periodic-sync-task",
using: nil
) { task in
IosBackgroundTaskHandler.shared.handleSingleTask(
task: task,
scheduler: scheduler,
executor: executor
)
}
// Register heavy processing task
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "heavy-processing-task",
using: nil
) { task in
IosBackgroundTaskHandler.shared.handleSingleTask(
task: task,
scheduler: scheduler,
executor: executor
)
}
// Register chain executor
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "kmp_chain_executor_task",
using: nil
) { task in
IosBackgroundTaskHandler.shared.handleChainExecutorTask(
task: task,
chainExecutor: chainExecutor
)
}
}
}Add this extension to handle background task scheduling when app enters background:
extension iOSApp {
func scenePhase(_ phase: ScenePhase) {
if phase == .background {
// iOS will execute scheduled tasks when app is in background
print("App entered background - BGTasks can now execute")
}
}
}Now you're ready to schedule your first background task!
class MyViewModel(
private val scheduler: BackgroundTaskScheduler
) {
// Your code here
}Or get it from Koin directly:
val scheduler: BackgroundTaskScheduler = get()suspend fun scheduleDataSync() {
val result = scheduler.enqueue(
id = "data-sync",
trigger = TaskTrigger.Periodic(
intervalMs = 15 * 60 * 1000 // 15 minutes
),
workerClassName = "SyncWorker",
constraints = Constraints(
requiresNetwork = true,
requiresCharging = false
)
)
when (result) {
ScheduleResult.ACCEPTED -> println("Task scheduled successfully!")
ScheduleResult.REJECTED_OS_POLICY -> println("OS rejected the task (e.g. battery saver, permissions)")
ScheduleResult.DEADLINE_ALREADY_PASSED -> println("Scheduled time is in the past")
ScheduleResult.THROTTLED -> println("Too many tasks scheduled")
}
}suspend fun uploadFile() {
scheduler.enqueue(
id = "file-upload",
trigger = TaskTrigger.OneTime(
initialDelayMs = 0 // Execute immediately
),
workerClassName = "UploadWorker",
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED, // WiFi only
backoffPolicy = BackoffPolicy.EXPONENTIAL,
backoffDelayMs = 10_000 // Retry after 10 seconds
)
)
}Now implement the actual work that will be executed in the background.
Add the worker logic to KmpWorker.kt (in androidMain):
class KmpWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val workerClassName = inputData.getString("workerClassName")
return when (workerClassName) {
"SyncWorker" -> executeSyncWorker()
"UploadWorker" -> executeUploadWorker()
else -> Result.failure()
}
}
private suspend fun executeSyncWorker(): Result {
return try {
// Your sync logic here
println("Syncing data from server...")
delay(2000)
// Emit event to notify UI
TaskEventBus.emit(
TaskCompletionEvent(
taskName = "SyncWorker",
success = true,
message = "✅ Data synced successfully"
)
)
Result.success()
} catch (e: Exception) {
Logger.e(LogTags.WORKER, "Sync failed", e)
Result.retry()
}
}
private suspend fun executeUploadWorker(): Result {
return try {
// Your upload logic here
println("Uploading file...")
delay(3000)
TaskEventBus.emit(
TaskCompletionEvent(
taskName = "UploadWorker",
success = true,
message = "✅ File uploaded"
)
)
Result.success()
} catch (e: Exception) {
Logger.e(LogTags.WORKER, "Upload failed", e)
Result.retry()
}
}
}Create worker classes in iosMain/background/workers/:
class SyncWorker : IosWorker {
override suspend fun doWork(
input: String?,
env: WorkerEnvironment
): WorkerResult {
return try {
// Your sync logic here (must complete within 25 seconds)
println("Syncing data from server...")
kotlinx.coroutines.delay(2000)
// Emit event to notify UI
TaskEventBus.emit(
TaskCompletionEvent(
taskName = "SyncWorker",
success = true,
message = "✅ Data synced successfully"
)
)
WorkerResult.Success("Synced")
} catch (e: Exception) {
Logger.e(LogTags.WORKER, "Sync failed", e)
WorkerResult.Failure("Failed")
}
}
}Register the worker in IosWorkerFactory.kt:
object IosWorkerFactory {
fun createWorker(className: String): IosWorker? {
return when (className) {
"SyncWorker" -> SyncWorker()
"UploadWorker" -> UploadWorker()
else -> null
}
}
}That's it! You now have KMP WorkManager set up. Here's what you can do next:
- Explore all triggers - Learn about 9 different trigger types
- Build task chains - Execute sequential and parallel workflows
- Configure constraints - Fine-tune when tasks run
- Platform-specific setup - Advanced Android & iOS configuration
- API Reference - Complete API documentation
- Check WorkManager initialization: Ensure Koin is properly initialized
- Check permissions: Verify all required permissions are in AndroidManifest.xml
- Check constraints: Tasks won't run if constraints aren't met (e.g., no network)
- Check Doze mode: Test with
adb shell dumpsys battery unplugandadb shell dumpsys deviceidle force-idle
- Check Info.plist: Ensure task identifiers are registered
- Check AppDelegate: Verify
registerBackgroundTasks()is called - App must be in background: BGTasks only run when app is backgrounded
- Test with simulator: Use
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"periodic-sync-task"]in LLDB - Check worker registration: Ensure worker is registered in
IosWorkerFactory
Make sure you're collecting events from TaskEventBus:
@Composable
fun MyScreen() {
LaunchedEffect(Unit) {
TaskEventBus.events.collect { event ->
println("Task event: ${event.taskName} - ${event.message}")
}
}
}- Read the API Reference
- Check the Platform Setup Guide
- Browse GitHub Issues
- Ask in GitHub Discussions
Happy scheduling! 🚀