Published: February 26, 2026
This version fixes some critical issues and improves performance. The main change is that chain.enqueue() is now a suspend function, which means you'll need to update your code.
Migration should take 10-30 minutes depending on your project size.
Breaking change: chain.enqueue() is now suspending
Why: The old blocking version could cause deadlocks in some scenarios. Making it suspending eliminates this issue entirely.
Performance improvements:
- HTTP operations are 60-86% faster (singleton HttpClient by default)
- Progress tracking is more reliable (flush interval reduced from 500ms to 100ms)
- New API to manually flush progress on iOS before app suspension
fun scheduleMyChain() {
val chain = scheduler.beginWith(
TaskRequest(workerClassName = "Step1Worker")
).then(
TaskRequest(workerClassName = "Step2Worker")
)
chain.enqueue() // This won't compile in v2.3.4
}Option 1: Make your function suspending
suspend fun scheduleMyChain() {
val chain = scheduler.beginWith(
TaskRequest(workerClassName = "Step1Worker")
).then(
TaskRequest(workerClassName = "Step2Worker")
)
chain.enqueue() // Works now
}Option 2: Wrap in a coroutine
fun scheduleMyChain() {
CoroutineScope(Dispatchers.IO).launch {
val chain = scheduler.beginWith(
TaskRequest(workerClassName = "Step1Worker")
).then(
TaskRequest(workerClassName = "Step2Worker")
)
chain.enqueue() // Works inside coroutine
}
}Option 3: Use the deprecated blocking version (temporary)
fun scheduleMyChain() {
val chain = scheduler.beginWith(
TaskRequest(workerClassName = "Step1Worker")
).then(
TaskRequest(workerClassName = "Step2Worker")
)
chain.enqueueBlocking() // Deprecated - will be removed in v3.0.0
}If you have integration tests that call enqueue(), wrap them in runBlocking:
@Test
fun testChainExecution() {
val chain = scheduler.beginWith(task1).then(task2)
chain.enqueue() // Won't compile
// assertions...
}@Test
fun testChainExecution() = runBlocking {
val chain = scheduler.beginWith(task1).then(task2)
chain.enqueue() // Works now
// assertions...
}Or make the test function suspend (if your test framework supports it):
@Test
suspend fun testChainExecution() {
val chain = scheduler.beginWith(task1).then(task2)
chain.enqueue()
// assertions...
}All built-in HTTP workers now use a singleton HttpClient by default. This makes HTTP operations 60-86% faster due to connection pooling and SSL session reuse.
You don't need to change anything - it's automatic.
If you were already providing a custom HttpClient, it will still be used:
val customClient = HttpClient { /* your config */ }
val worker = HttpRequestWorker(httpClient = customClient)v2.3.4 adds a new API to flush pending progress updates before your app goes to the background. This prevents data loss on iOS.
Add this to your AppDelegate:
import UIKit
import KMPWorkManager
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillResignActive(_ application: UIApplication) {
// Flush progress before app enters background
KmpWorkManager.shared.backgroundTaskScheduler.flushPendingProgress()
}
func applicationWillTerminate(_ application: UIApplication) {
// Also flush on termination (rare but possible)
KmpWorkManager.shared.backgroundTaskScheduler.flushPendingProgress()
}
}This takes 10-50ms and guarantees no progress data is lost when iOS suspends your app.
On Android, this is a no-op (WorkManager handles persistence automatically).
// build.gradle.kts
dependencies {
implementation("dev.brewkits:kmpworkmanager:2.3.4") // Update from 2.3.3
}./gradlew buildYou'll see errors like:
Suspend function 'enqueue' should be called only from a coroutine or another suspend functionFor each error, pick one of these fixes:
Make the calling function suspend:
suspend fun myFunction() {
chain.enqueue()
}Or wrap in a coroutine:
fun myFunction() {
CoroutineScope(Dispatchers.IO).launch {
chain.enqueue()
}
}Or use the deprecated blocking version (temporary):
fun myFunction() {
chain.enqueueBlocking() // Works but deprecated
}Wrap test functions in runBlocking:
@Test
fun testSomething() = runBlocking {
chain.enqueue()
}./gradlew clean build
./gradlew testclass MyActivity : AppCompatActivity() {
fun scheduleWork() {
lifecycleScope.launch {
val chain = scheduler.beginWith(task)
chain.enqueue()
}
}
}class MyViewModel : ViewModel() {
fun scheduleWork() {
viewModelScope.launch {
val chain = scheduler.beginWith(task)
chain.enqueue()
}
}
}struct ContentView: View {
func scheduleWork() {
Task {
let chain = scheduler.beginWith(task: task)
try await chain.enqueue()
}
}
}class MyViewController: UIViewController {
func scheduleWork() {
Task {
let chain = scheduler.beginWith(task: task)
try await chain.enqueue()
}
}
}// Make it suspend:
suspend fun scheduleDataSync() {
scheduler.beginWith(
TaskRequest("DownloadWorker")
).then(
TaskRequest("ProcessWorker")
).then(
TaskRequest("UploadWorker")
).enqueue()
}
// Or wrap in launch:
fun scheduleDataSync() {
CoroutineScope(Dispatchers.IO).launch {
scheduler.beginWith(
TaskRequest("DownloadWorker")
).then(
TaskRequest("ProcessWorker")
).then(
TaskRequest("UploadWorker")
).enqueue()
}
}class ChainBuilder(private val scheduler: BackgroundTaskScheduler) {
suspend fun buildAndEnqueue() { // Made suspending
val chain = scheduler.beginWith(task1)
.then(task2)
.then(task3)
chain.enqueue()
}
}suspend fun scheduleConditional(includeStep2: Boolean) {
var chain = scheduler.beginWith(task1)
if (includeStep2) {
chain = chain.then(task2)
}
chain.enqueue()
}Error: "Suspend function should be called only from a coroutine"
Wrap in launch or make the function suspending.
Error: "Type mismatch: inferred type is Unit but TestResult was expected"
Add = runBlocking to your test function.
Warning: "enqueueBlocking() is deprecated"
Switch to using suspending enqueue() with proper coroutine handling.
- 60-86% faster HTTP operations (automatic)
- Zero deadlock risk (eliminated blocking code)
- 90% less progress data loss on iOS (shorter flush interval + manual API)
- Better resource usage (connection pooling)
- Cleaner async code (proper suspend functions)
- Ready for v3.0.0
dependencies {
implementation("dev.brewkits:kmpworkmanager:2.3.3")
}Then rebuild:
./gradlew clean build- Small projects (< 10 chain calls): 10-15 minutes
- Medium projects (10-50 chain calls): 20-30 minutes
- Large projects (50+ chain calls): 30-60 minutes
Tip: Use your IDE's "Find Usages" to locate all enqueue() calls, then update them one by one.
- GitHub Issues: https://github.com/brewkits/kmpworkmanager/issues
- Email: datacenter111@gmail.com
Last updated: February 26, 2026