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