Skip to content
Go back

Sleeping But Working: Cross-Platform Background Sync with KMP

by KMP Bits

Cover image for Sleeping But Working: Background Sync in KMP

Also available on Medium

✏️ Introduction

Running reliable background tasks is one of those things that sounds simple until you do it cross-platform. If you’re building a Kotlin Multiplatform (KMP) app, and you want background syncs for fresh data, offline caching, or periodic uploads, you quickly hit a wall: there’s no common API for background work out of the box.

In this article, I’ll show you how to bridge that gap using WorkManager on Android and BGTaskScheduler on iOS with a real demo you can clone and try out: https://github.com/kmpbits/BackgroundSchedulerKMPDemo.

Let’s break it down, KMP-style.


⚙️ Why Background Sync is Tricky in KMP

Background work is a classic case of the same goal, different platforms, no shared API.


🤖 WorkManager on Android

What is WorkManager? WorkManager is Android Jetpack’s modern solution for scheduling background tasks that are guaranteed to run… eventually. It handles battery optimizations, Doze mode, and system restarts.

In my demo, I use WorkManager to:

Here’s how the Android side works:

1: Add the WorkManager dependency to libs.version.toml:

workmanager = "2.10.2"

[libraries]
workmanager = { module = "androidx.work:work-runtime-ktx", version.ref = "workmanager" }

2: Add it to the build.gradle.kts file, on androidMain block:

androidMain.dependencies {
     // Other dependencies...
     implementation(libs.workmanager)
}

3: Create the PrintLogWorker class

class PrintLogWorker(
    context: Context,
    workerParameters: WorkerParameters
) : Worker(context, workerParameters) {

    override fun doWork(): Result {
        printLog("Android App Refresh Task")

        return Result.success()
    }
}

4: Create the instance of the WorkManager and enqueue the work every 15 minutes

fun initWorker(context: Context) {
    val workManagerBuilder = PeriodicWorkRequestBuilder<PrintLogWorker>(
        repeatInterval = Constants.TASK_TIMER_SECONDS.toLong(),
        repeatIntervalTimeUnit = TimeUnit.MINUTES
    )
        // Optional: Set constraints for the worker
        .setConstraints(
            Constraints.Builder()
                .setRequiresBatteryNotLow(true)
                .build()
        )

    // Enqueue the work every 15 minutes
    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        uniqueWorkName = Constants.TASK_IDENTIFIER,
        existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP,
        request = workManagerBuilder.build()
    )
}

5: Add the initWorker() function to the Application’s onCreate() method.

class ComposeApp : Application() {

    override fun onCreate() {
        super.onCreate()

        initWorker(this)
    }
}

Note: Don’t forgot to add the ComposeApp to the Manifest file.


🍏 BGTaskScheduler on iOS

On iOS, things are more restrictive: you have to add the background mode capability, register at launch, and you can’t schedule them too frequently, iOS decides when they run.

I use BGAppRefreshTask to refresh app data in the background.

Here’s the code for iOS:

1: Add the Background Modes capability

On XCode: iosApp > Signing & Capabilities > + button > Search: Background Modes. Then check Background fetch and Background processing

XCode Background Mode capability

2: Create the worker in scheduleAppRefreshTask() and register it on registerTask()

Note: Inside the registerForTaskWithIdentifier block, you have to call the code to run on background and then call the scheduleAppRefreshTask() again. BGTaskScheduler doesn’t allow periodic work automatically.

fun registerTask() {
    BGTaskScheduler.sharedScheduler.registerForTaskWithIdentifier(
        identifier = Constants.TASK_IDENTIFIER,
        usingQueue = null
    ) { task ->
        // Call the function to do the work on background
        printLog("iOS App Refresh Task")

        // Then call the function to schedule the task again
        scheduleAppRefreshTask()
        (task as? BGAppRefreshTask)?.setTaskCompletedWithSuccess(true)
    }
}

@OptIn(ExperimentalForeignApi::class)
private fun scheduleAppRefreshTask() {
    val request = BGAppRefreshTaskRequest(identifier = Constants.TASK_IDENTIFIER)
    request.earliestBeginDate = NSDate().dateByAddingTimeInterval((Constants.TASK_TIMER_SECONDS * 60).toDouble()) // 15 minutes
    BGTaskScheduler.sharedScheduler.submitTaskRequest(request, null)
}

3: Call the registerTask() on iOSApp.swift

class AppDelegate: NSObject, UIApplicationDelegate {
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        
         // Other initializations...

         BackgroundWorkTaskIOSKt.registerTask()

         return true
     }
}

💡 Gotchas & Tips


🚀 Demo Project

I’ve put all of this into a small working demo you can fork and run. The demo app logs a message to Logcat roughly every 15 minutes.

Note: the exact timing depends on device conditions — the OS ultimately decides when to run.

📌 Repo: https://github.com/kmpbits/BackgroundSchedulerKMPDemo

Note: To test, you have to run the app on a real device.


🔮 Next Steps & Improvements


✅ Conclusion

Background syncs are more work in cross-platform, but KMP’s androidMain and iOSMain makes it doable. Keep your shared logic clean, respect platform constraints, and test well.

If you found this useful, follow me on Medium for more practical KMP tips.


Share this post on:

Previous Post
No, Emitting Loading State from the Repository Doesn’t Make You a Junior Dev
Next Post
🧊 How Talking to Myself, Vanilla PHP, and an Ice Cream Led Me to Kotlin Multiplatform