Android launch sequence¶
The Android launch sequence is two steps — register, then start — plus one mandatory wiring: tell WorkManager to use Backgrounder's WorkerFactory via Configuration.Provider. Construction is automatic: the library registers an androidx.startup initializer that builds BackgroundTaskManager.shared before Application.onCreate runs.
import androidx.work.Configuration
import com.happycodelucky.backgrounder.BackgroundTaskManager
import com.happycodelucky.backgrounder.androidWorkerFactory
import com.happycodelucky.backgrounder.shared
class MyApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
// 1. BackgroundTaskManager.shared already exists: the library's androidx.startup
// initializer built it before onCreate ran. If your manifest removes
// the InitializationProvider entirely, call
// BackgroundTaskManager.configure(application = this) here first.
// 2. Register every worker factory. The closure is yours — resolve
// dependencies however you like (Koin, Hilt, hand-wired).
BackgroundTaskManager.shared.register(SyncWorker.ID) {
SyncWorker(repo = appGraph.repository)
}
// 3. Start. Sweeps ephemeral work left over from the previous process,
// seals the registry, and flips the ready gate so workers enqueued
// before this point may now dispatch.
BackgroundTaskManager.shared.start()
}
// Tell WorkManager to use Backgrounder's WorkerFactory. Required.
override val workManagerConfiguration: Configuration get() =
Configuration.Builder()
.setWorkerFactory(BackgroundTaskManager.shared.androidWorkerFactory())
.build()
}
AndroidManifest¶
Disable WorkManager's default auto-init, which is mandatory whenever you implement Configuration.Provider. Remove only WorkManager's meta-data entry, not the provider: BackgroundTaskManager's own BackgrounderInitializer is registered on the same provider and is what populates BackgroundTaskManager.shared. If your app removes the provider altogether, call BackgroundTaskManager.configure(application = this) at the top of onCreate instead.
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
Without this, WorkManager's auto-init content provider runs before Application.onCreate and locks the Configuration to its defaults — your workManagerConfiguration override would never be consulted.
Construction order¶
Configuration.Provider.workManagerConfiguration is invoked the first time anyone calls WorkManager.getInstance(context), and it reads BackgroundTaskManager.shared.androidWorkerFactory(). The startup initializer builds BackgroundTaskManager.shared while content providers are created, which is before Application.onCreate, so the instance always exists by then. Construction itself never touches WorkManager; the ephemeral sweep that does runs inside start().
If you call BackgroundTaskManager.configure(application = this) yourself (see below), do it at the top of onCreate, before anything can trigger WorkManager.getInstance.
What runs where¶
RegistryDispatchWorker(the single Worker class registered with WorkManager) is dispatched on WorkManager's executor — effectivelyDispatchers.Default.- The worker's
execute()runs on a coroutine that inherits that dispatcher; switch withwithContext(Dispatchers.IO)for blocking IO. - Logs from inside the worker are tagged
Backgrounder/<taskId>and the thread is namedBackgrounder/<taskId>for the duration ofexecute(). This is the mitigation for the single-bridge-worker design — every log includes the task id even though the Worker class is the same for every task.
Constraints are native¶
Android is the platform where WorkConstraints carries real OS weight. networkRequired, requiresCharging, and requiresDeviceIdle all map straight onto androidx.work.Constraints and are enforced by JobScheduler — WorkManager will not dispatch the worker until every constraint holds, holding it indefinitely if needed.
requiresDeviceIdle = true in particular has no equivalent on the other targets: it maps to Constraints.Builder.setRequiresDeviceIdle(true), so the work runs only when the device enters idle / a Doze maintenance window. A task waiting on it reports PendingPredicate.RequiresDeviceIdle from backgrounder.scheduled(). On iOS / macOS / JVM the same flag is advisory and ignored — see Opportunistic dispatch and Require the device to be idle.
Multi-process apps¶
The androidx.startup provider runs only in the main process, so in a :remote process BackgroundTaskManager.shared is not populated automatically. Call BackgroundTaskManager.configure(application = this) at the top of Application.onCreate, which runs in every process. The call is idempotent with the initializer: when an instance already exists and you pass no listener or WorkManager, it returns that instance. Each process holds its own BackgroundTaskManager, but they share the same WorkManager database, so scheduled work is consistent across processes.