I get the following exception for 2 of my CoroutineWorkers with additional parameters injected by Hilt. This does not happen for workers without additional parameters and only happens when I install the app via APK by my fastlane build. Android Studio builds work perfectly. But my build pipeline uses the same settings as Android Studio builds. By work, I mean that the workers get enqueued and do not throw the following exception...
Could not instantiate com.example.android.ActivitySyncWorker
java.lang.NoSuchMethodException:
com.example.android.ActivitySyncWorker.<init> [class android.content.Context, class androidx.work.WorkerParameters]
at java.lang.Class.getConstructor0(Class.java:3325)
at java.lang.Class.getDeclaredConstructor(Class.java:3063)
at androidx.work.WorkerFactory.createWorkerWithDefaultFallback(WorkerFactory.java:94)
at androidx.work.impl.WorkerWrapper.runWorker(WorkerWrapper.java:243)
at androidx.work.impl.WorkerWrapper.run(WorkerWrapper.java:144)
at androidx.work.impl.utils.SerialExecutorImpl$Task.run(SerialExecutorImpl.java:96)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
The internal variable of Hilt mWorkerFactories
is empty. My worker cannot be found then. But with Android Studio builds it's not empty.
Before I continue. What makes this question different from other similar ones is, that I have a multi-module project. Maybe I am missing something because of it.
This is the Worker inside the app module :core-ble
@HiltWorker
class ActivitySyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
val bleServiceModule: BleServiceModule,
private val connectionPool: BleConnectionPool,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return coroutineScope {
try {
// ...
Result.success()
} catch (exception: Exception) {
SLog.e("[BLE_SYNC] worker error: $exception")
if (runAttemptCount < 2) {
Result.retry()
} else {
Result.failure()
}
}
}
}
companion object {
private const val ACTIVITY_SYNC_WORKER_TAG = "activity_sync"
fun enqueue(context: Context, workManager: WorkManager, workerParams: Data = Data.EMPTY) {
val workRequest = PeriodicWorkRequestBuilder<ActivitySyncWorker>(30, TimeUnit.MINUTES).apply {
addTag(ACTIVITY_SYNC_WORKER_TAG)
setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
setInputData(
Data.Builder()
.putAll(workerParams)
.build()
)
}.build()
workManager.enqueueUniquePeriodicWork(
/* uniqueWorkName = */ ACTIVITY_SYNC_WORKER_TAG,
/* existingPeriodicWorkPolicy = */ ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
/* periodicWork = */ workRequest
)
}
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(ACTIVITY_SYNC_WORKER_TAG)
}
}
}
This worker gets enqueued always, but has no additional injected parameters. Because of reflection, it can be found.
@HiltWorker
class RemoteConfigSyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return coroutineScope {
try {
// ...
Result.success()
} catch (exception: Exception) {
if (runAttemptCount < 2) {
Result.retry()
} else {
Result.failure()
}
}
}
}
companion object {
private const val REMOTE_CONFIG_SYNC_WORKER_TAG = "remote_config_sync"
fun enqueue(workManager: WorkManager, workerParams: Data = Data.EMPTY) {
val workRequest = PeriodicWorkRequestBuilder<RemoteConfigSyncWorker>(30, TimeUnit.MINUTES).apply {
addTag(REMOTE_CONFIG_SYNC_WORKER_TAG)
setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
setInputData(
Data.Builder()
.putAll(workerParams)
.build()
)
}.build()
workManager.enqueueUniquePeriodicWork(
/* uniqueWorkName = */ REMOTE_CONFIG_SYNC_WORKER_TAG,
/* existingPeriodicWorkPolicy = */ ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
/* periodicWork = */ workRequest
)
}
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(REMOTE_CONFIG_SYNC_WORKER_TAG)
}
}
}
As you see in my Manifest, I removed the default Initializer and added my own...
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="com.example.android.WorkManagerModule"
android:value="androidx.startup" />
</provider>
This is the new Initializer...
@Module
@InstallIn(SingletonComponent::class)
object WorkManagerModule : Initializer<WorkManager> {
private var isInitialized = false
@Provides
@Singleton
override fun create(@ApplicationContext context: Context): WorkManager {
if (!isInitialized) { // just in case this gets called twice
val entryPoint = EntryPointAccessors.fromApplication(
context,
HiltWorkerFactoryEntryPoint::class.java
)
val configuration = Configuration.Builder()
.setWorkerFactory(entryPoint.workerFactory())
.setMinimumLoggingLevel(Log.VERBOSE)
.build()
WorkManager.initialize(context, configuration)
isInitialized = true
}
return WorkManager.getInstance(context)
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface HiltWorkerFactoryEntryPoint {
fun workerFactory(): HiltWorkerFactory
}
}
I do this in onCreate of my MultiDexApplication which is located in :app
AppInitializer.getInstance(this).initializeComponent(WorkManagerModule::class.java)
My imports... (in both :core-ble
and :app
.
implementation(libs.hilt)
implementation(libs.hilt.work)
implementation(libs.hilt.plugin)
kapt(libs.hilt.compiler)
kapt(libs.hilt.androidx.compiler)
implementation(libs.work.runtime.ktx)
implementation(libs.startup)
hilt = "2.44.2"
hiltAndroidX = "1.1.0"
hiltCompose = "1.0.0"
hiltWork = "1.0.0"
work = "2.9.0"
hiltKapt = "2.35"
hilt-plugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" }
hilt-kapt = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hiltKapt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-androidx-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltAndroidX" }
hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltWork" }
startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "startup" }
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
work-runtime = { group = "androidx.work", name = "work-runtime", version.ref = "work" }
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
I also tried adding these ProGuard rules, but why should it work with Android Studio builds but not with APK builds.
-keepclassmembers class * extends androidx.work.Worker {
public <init>(android.content.Context,androidx.work.WorkerParameters);
}
-keepclassmembers class * extends androidx.work.CoroutineWorker {
public <init>(android.content.Context,androidx.work.WorkerParameters);
}
This is my hilt module for one of the parameters of the WorkManagers. Same for the parameters inside provideBleServiceModule
@Module
@InstallIn(SingletonComponent::class)
object BleServiceModuleModule {
@Singleton
@Provides
fun provideBleServiceModule(
@ApplicationContext context: Context,
sharedPrefHelper: CommonsSharedPrefHelper,
bluetoothAdapterStateModule: BluetoothAdapterStateModule,
userDbService: UserDbService,
bleEvents: BleEvents,
): BleServiceModule = BleServiceModule(
context,
sharedPrefHelper,
bluetoothAdapterStateModule,
userDbService,
bleEvents,
)
}
I ended up writing my own WorkerFactory and injecting the necessary parameters there...
class BleWorkerFactory @Inject constructor(
private val bleServiceModule: BleServiceModule,
) : WorkerFactory() {
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
return when (workerClassName) {
RemoteConfigSyncWorker::class.java.name -> RemoteConfigSyncWorker(appContext, workerParameters)
HardwareReportWorker::class.java.name -> HardwareReportWorker(appContext, workerParameters, bleServiceModule)
ActivitySyncWorker::class.java.name -> ActivitySyncWorker(appContext, workerParameters, bleServiceModule)
else -> null
}
}
}
@Singleton
class MyDelegatingWorkerFactory @Inject constructor(
bleWorkerFactory: BleWorkerFactory
) : DelegatingWorkerFactory() {
init {
addFactory(bleWorkerFactory)
}
}
In my application I do not implement Configuration.Provider since I got a RuntimeException: Unable to instantiate application
when overriding the workManagerConfiguration val. Instead I set the configuration in onCreate().
@HiltAndroidApp
open class RandomHiltApplication : MyApplication() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WorkerFactoryEntryPoint {
fun workerFactory(): MyDelegatingWorkerFactory
}
override fun onCreate() {
val workManagerConfiguration: Configuration = Configuration.Builder()
.setWorkerFactory(EntryPoints.get(this, WorkerFactoryEntryPoint::class.java).workerFactory())
.setMinimumLoggingLevel(Log.VERBOSE)
.build()
WorkManager.initialize(this, workManagerConfiguration)
super.onCreate()
}
}
My Fastlane builds are now working and I am happy with it. Sadly, I couldn't find out, why the HiltWorkerFactory is not working.