Featured image of post WorkManager: Worker with Koin injection.

WorkManager: Worker with Koin injection.

Introduction

Let’s write a simple Proof Of Concept app that uses WorkManager to schedule periodic Work - checking and logging battery level. We will use Koin to manage dependency injection.

Writing the app

Start with a new Android Studio project.

Add Koin and WorkManager dependency

WorkManagerKoinInjection/gradle/libs.versions.toml:

[versions]
...
koinBom = "3.5.6"
workManagerVersion = "2.10.3"

[libraries]
...
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBom" }
koin-core = { module = "io.insert-koin:koin-core" }
koin-android = { module = "io.insert-koin:koin-android" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" }
koin-androidx-workmanager = { module = "io.insert-koin:koin-androidx-workmanager" }
work-manager = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManagerVersion" }

WorkManagerKoinInjection/app/build.gradle.kts:

dependencies {
    ...
    implementation(platform(libs.koin.bom))
    implementation(libs.koin.core)
    implementation(libs.koin.android)
    implementation(libs.koin.androidx.workmanager)
    implementation(libs.work.manager)

Check battery level

This is our dummy work we want the WorkManager to periodically execute. We will create BatteryApi interface and implementation will be BatteryService. WorkManagerKoinInjection/app/src/main/java/com/xstmpx/workmanagerkoininjection/BatteryApi.kt

interface BatteryApi {
    fun getBatteryLevel(): Float
}

WorkManagerKoinInjection/app/src/main/java/com/xstmpx/workmanagerkoininjection/BatteryService.kt

class BatteryService(
    private val context: Context
) : BatteryApi {
    override fun getBatteryLevel(): Float {
        val batteryStatus: Intent? =
            IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { intentFilter ->
                context.registerReceiver(null, intentFilter)
            }
        val batteryLevel: Float = batteryStatus?.let { intent ->
            val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
            val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
            level * 100 / scale.toFloat()
        } ?: 0F
        return batteryLevel
    }
}

Create Application class and initialize Koin

WorkManagerKoinInjection/app/src/main/java/com/xstmpx/workmanagerkoininjection/WorkManagerDemoApplication.kt


class WorkManagerDemoApplication : Application(), KoinComponent {
    val appModule = module {
        singleOf(::BatteryService) { bind<BatteryApi>() }
    }

    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@WorkManagerDemoApplication)
            modules(appModule)
        }
    }
}

/WorkManagerKoinInjection/app/src/main/AndroidManifest.xml


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

    <application
        android:name=".WorkManagerDemoApplication"
        ...

Log battery level

Now let’s test if the BatteryService works and if Koin injects as desired. We can inject BatteryApi in MainActivity and log level in onCreate():

class MainActivity : ComponentActivity() {
    private val batteryApi: BatteryApi by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val batteryLevel = batteryApi.getBatteryLevel()
        Log.e("ASDF", "BatteryLevel: $batteryLevel")
        ...

In logcat I can see:

ASDF E  BatteryLevel: 100.0

Create a Worker - WorkManager

Now that we have the logic and dependency injection working we can create a Worker that will execute our Battery logging work periodically. This, however is where the problem begins and Koin comes to the rescue.

Problem

Usually we define a Worker like this:

class BatteryLoggerWorker(
    applicationContext: Context,
    workerParameters: WorkerParameters,
) : CoroutineWorker(applicationContext, workerParameters) {

    override suspend fun doWork(): Result {
        doSomething()
        return Result.success()
    }
}

We then create a work request:

val batteryLoggerWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<BatteryLoggerWorker>()
       .build()

And submit to the WorkManager:

WorkManager
    .getInstance(myContext)
    .enqueue(batteryLoggerWorkRequest)

This means that we do not create an instance of BatteryLoggerWorker ourselves. This means that we cannot just inject BatteryApi in the constructor.

Solution

Fortunately Koin helps with that, but we still need to add some boilerplate code to our manifest and application class. We need to add InitializationProvider to AndroidManifest.xml

...
        </activity>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <!-- If you are using androidx.startup to initialize other components -->
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup"
                tools:node="remove" />
        </provider>
</application>
...

WorkManagerDemoApplication.kt

...
override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@WorkManagerDemoApplication)
            modules(appModule)
            //Add code below ↓↓↓
            workManagerFactory()
        }

        setupWorkManagerFactory()
    }

    val workerFactory: DelegatingWorkerFactory = DelegatingWorkerFactory()

    fun Application.setupWorkManagerFactory() {
        getKoin().getAll<WorkerFactory>()
            .forEach {
                workerFactory.addFactory(it)
            }
    }

Create BatteryLoggerWorker

Now we will be able to inject our BatteryApi in the worker: WorkManagerKoinInjection/app/src/main/java/com/xstmpx/workmanagerkoininjection/BatteryLoggerWorker.kt

class BatteryLoggerWorker(
    applicationContext: Context,
    workerParameters: WorkerParameters,
    private val batteryApi: BatteryApi
) : CoroutineWorker(applicationContext, workerParameters) {

    override suspend fun doWork(): Result {
        val batteryLevel = batteryApi.getBatteryLevel()
        Log.e("BatteryLoggerWorker", "BatteryLevel: $batteryLevel")
        return Result.success()
    }
}

In our WorkManagerDemoApplication we can declare a worker:

val appModule = module {
        singleOf(::BatteryService) { bind<BatteryApi>() }
        workerOf(::BatteryLoggerWorker)
}

Schedule periodic work

Finally we can schedule our work. We can do it in our MainActivity.

...
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        val batteryLevel = batteryApi.getBatteryLevel()
//        Log.e("ASDF", "BatteryLevel: $batteryLevel")
        scheduleBatteryLoggerWork()
...
fun Context.scheduleBatteryLoggerWork() {
    val tokenWorker = PeriodicWorkRequestBuilder<BatteryLoggerWorker>(
        15, TimeUnit.MINUTES,
        5, TimeUnit.MINUTES,
    ).build()
    val result = WorkManager.getInstance(this)
        .enqueue(tokenWorker)
    Log.e("MainActivity", "WORK Schedule result: ${result.result}")
}
...

The logger says:

14:05:32 MainActivity            E  WORK Schedule result: androidx.concurrent.futures.CallbackToFutureAdapter$SafeFuture$1@bdf1589[status=PENDING, info=[tag=[kotlin.Unit]]]
14:15:32 WM-DelayedWorkTracker   D  Scheduling work 9b4c4df7-346d-4b64-833e-66b772c19d3c
14:15:32 WM-GreedyScheduler      D  Starting work for 9b4c4df7-346d-4b64-833e-66b772c19d3c
14:15:32 WM-Processor            D  Processor: processing WorkGenerationalId(workSpecId=9b4c4df7-346d-4b64-833e-66b772c19d3c, generation=0)
14:15:32 WM-WorkerWrapper        D  Starting work for com.xstmpx.workmanagerkoininjection.BatteryLoggerWorker
14:15:32 BatteryLoggerWorker     E  BatteryLevel: 100.0
14:15:32 WM-WorkerWrapper        I  Worker result SUCCESS for Work [ id=9b4c4df7-346d-4b64-833e-66b772c19d3c, tags={ com.xstmpx.workmanagerkoininjection.BatteryLoggerWorker } ]
14:15:32 WM-Processor            D  Processor 9b4c4df7-346d-4b64-833e-66b772c19d3c executed; reschedule = false
14:15:32 WM-GreedyScheduler      D  Cancelling work ID 9b4c4df7-346d-4b64-833e-66b772c19d3c
14:15:32 WM-SystemJobScheduler   D  Scheduling work ID 9b4c4df7-346d-4b64-833e-66b772c19d3cJob ID 1
14:30:33 WM-DelayedWorkTracker   D  Scheduling work 9b4c4df7-346d-4b64-833e-66b772c19d3c
14:30:33 WM-GreedyScheduler      D  Starting work for 9b4c4df7-346d-4b64-833e-66b772c19d3c
14:30:33 WM-Processor            D  Processor: processing WorkGenerationalId(workSpecId=9b4c4df7-346d-4b64-833e-66b772c19d3c, generation=0)
14:30:33 WM-WorkerWrapper        D  Starting work for com.xstmpx.workmanagerkoininjection.BatteryLoggerWorker
14:30:33 BatteryLoggerWorker     E  BatteryLevel: 100.0
14:30:33 WM-WorkerWrapper        I  Worker result SUCCESS for Work [ id=9b4c4df7-346d-4b64-833e-66b772c19d3c, tags={ com.xstmpx.workmanagerkoininjection.BatteryLoggerWorker } ]
14:30:33 WM-Processor            D  Processor 9b4c4df7-346d-4b64-833e-66b772c19d3c executed; reschedule = false
14:30:33 WM-GreedyScheduler      D  Cancelling work ID 9b4c4df7-346d-4b64-833e-66b772c19d3c
14:30:33 WM-SystemJobScheduler   D  Scheduling work ID 9b4c4df7-346d-4b64-833e-66b772c19d3cJob ID 2

Summary

Full code