I'm using kotlin for android and I'm trying to create a generic Worker class in which I can pass a lambda which can be called from doWork() method.
class BaseWorker(val context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
//Passed from the activity while creating the work request
someLambda()
return Result.success()
}
}
The problem is I am not instantiating the BaseWorker class by calling the constructor.
Is it possible to pass the Lambda using setInputData() of OneTimeWorkRequestBuilder class.
I referred How to pass the worker parameters to WorkManager class where the constructor of the class is being called which I think is not the right way.
The Data class in WorkManager it's only intended for base types and their arrays. You cannot use it to pass a lamba.
A possible solution is to customize WorkManager's initialization, as explained in the documentation, and use a custom WorkerFactory to add a parameter to the constructor that you can use to retrieve the lambda. Keep in mind that you are configuring WorkManager just once, at initialization time. This means that you can pass directly the lambda as the additional parameter, but it will not be possible to customize it for each WorkRequest.
Depending on what you want to achieve exactly, something similar can be used as a starting point:
// provide custom configuration
val config = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.setWorkerFactory(MyWorkerFactory(lambda))
.build()
//initialize WorkManager
WorkManager.initialize(this, config)
val workManager = WorkManager.getInstance()
And then have your WorkerFactory:
class MyWorkerFactory(private val lambda: Unit) : WorkerFactory() {
override fun createWorker(appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters): MyWorker {
return MyWorker(appContext, workerParameters, lambda)
}
}
You can then have your worker that use the new constructor:
class MyWorker(val context: Context, workerParams: WorkerParameters, private val lambda: Unit) : Worker(context, workerParams) {
override fun doWork(): Result {
//Passed from the WorkManager's configuration
lambda()
return Result.success()
}
}
Remember to disable the default WorkManager initialization adding to the AndroidManifest.xml:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
I think it's worth revisiting your question to see what you are trying to do and why it's fundamentally flawed. You are trying to pass a lambda from an Activity to a worker, which is something that runs in the background, even if the Activity is no longer around. This doesn't make any sense. Please don't do this - it's only going to result in memory leaks, crashes, and/or strange errors that will be hard for you to track down. Remember that workers need to be able to be created from scratch when the OS tells your app to run them.
Actually, you CAN do it ONLY using what WorkManager provides to us. IMO changing the WorkManager initialization is too complex/risk for a simple thing like this as suggested in the other answer.
WorkRequests accept ByteArray input, which can be any object, right? So, create a serialized object wrapping the lambda function that will be later called
Define a serializable wrapper class passing the lambda(logRequestTime): LambdaSerializable
package com.febaisi.lambdawithworkers
import java.io.Serializable
class LambdaSerializable(val logRequestTime: () -> (Unit)): Serializable {
}
Convert it to ByteArray and put it in the input object.
val lambdaObject = LambdaSerializable { //Lambda function
Log.e("Lambda", "Request time was -> $requestTime")
}
val input = Data.Builder()
.putByteArray(SimpleWorker.LAMBDA_OBJECT, serializeObject((lambdaObject as Object)))
.build()
val simpleWorker = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInputData(input)
.build()
WorkManager.getInstance(applicationContext).enqueueUniqueWork("lambda_worker", ExistingWorkPolicy.KEEP, simpleWorker)
Call it from the worker. SimpleWorker
class SimpleWorker(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
companion object {
val LAMBDA_OBJECT = "LAMBDA_OBJECT"
}
override fun doWork(): Result {
Log.e("Lambda", "Executing worker - Sleeping for 5 seconds - Compare request vs current time")
val lambdaSerializable = inputData.getByteArray(LAMBDA_OBJECT)?.let{ getByteInput(it) }
runBlocking {
delay(5000)
val sdf = SimpleDateFormat("yyyyMMdd__HH:mm:ss", Locale.getDefault())
Log.e("Lambda", "Current time is -> ${sdf.format(Date())}")
(lambdaSerializable as LambdaSerializable).logRequestTime() // High level - Calling Lambda
}
return Result.success()
}
}
FULL EXAMPLE HERE: https://github.com/febaisi/LambdaWithWorkers/
Check logs to see lambda function being called.
Related
These two methods invoke the same use-case. In the first version, I hard-coded Dispatchers.IO, and things work as expected.
The second version (which I prefer) uses an injected dispatcher that defaults to the Dispatchers.IO type. It fails with the IllegalStateException described in the comments. Any ideas?
#HiltViewModel
class MainViewModel #Inject constructor(
private val getUsers: GetUsers,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
val liveData: MutableLiveData<List<User>> = MutableLiveData()
suspend fun getUsersByParamDispatcher(params: GetUsers.Params) {
// Successfully works as intended.
viewModelScope.launch(Dispatchers.IO) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
suspend fun getUsersByInjectDispatcher(params: GetUsers.Params) {
// IllegalStateException: Cannot access database on the main thread since it may potentially
// lock the UI for a long period of time.
// at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:494).
viewModelScope.launch(dispatcher) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
}
Logs confirm the exception and my curiosity is why are they different and how would I arrive at a working injected version.
Failing injected Dispatchers.IO:
>> coroutine.name: main
Working parameter Dispatchers.IO:
>> coroutine.name: DefaultDispatcher-worker-1
The dependencies are provided by #HiltViewModel and I expect dispatcher to respect its assigned default value. The Fragment creates this view model with the by viewModels() delegate.
It might be fine to hard-code the dispatcher. But with injection a blocking TestCoroutineDispatcher is easily passed during testing.
Maybe I'm overlooking something simple, or another way altogether.
// MainViewModelTest
#Before
fun setup() {
MockKAnnotations.init(this)
viewModel = MainViewModel(
getUsers,
coroutinesTestRule.testDispatcher
)
}
I've been using workManager and it works fine with dagger2 but when i used WorkContinuation.combine() for chaining there was an issue in WorkerFactory
My workers didn't initialze
crash report is
java.lang.IllegalArgumentException: unknown worker class name: androidx.work.impl.workers.CombineContinuationsWorker
at incubasys.aydo.repositories.worker.WorkerFactory.createWorker(WorkerFactory.kt:22)
at androidx.work.WorkerFactory.createWorkerWithDefaultFallback(WorkerFactory.java:81)
at androidx.work.impl.WorkerWrapper.runWorker(WorkerWrapper.java:228)
at androidx.work.impl.WorkerWrapper.run(WorkerWrapper.java:127)
at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:75)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
WorkerFactory where the crash happens
class DaggerWorkerFactory #Inject constructor(
private val workerFactories: Map<Class<out ListenableWorker>, #JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val foundEntry =
workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
//crashes here
val factoryProvider = foundEntry?.value
?: throw IllegalArgumentException("unknown worker class name: $workerClassName")
return factoryProvider.get().create(appContext, workerParameters)
}
}
it only crashes when i use WorkContinuation.combine().
WorkContinuation.combine() adds an instance of the CombineContinuationsWorker class to your continuation.
This means that your WorkerFactory needs to handle this class names when creating the actual worker class.
You have two options:
You handle CombineContinuationsWorker to your workerFactory
You use a delegatingWorkerFactory and add your factory to it so that it returns null when it does not find the correct class name.
The delegatingWorkerFactory will then use the default workerFactory to instantiate the right class.
Personally I prefer the second option as it handles whatever will change in WorkManager and you can potentially add more/different Worker Factories that handle different pieces/module of your application.
You should return null from your WorkerFactory to delegate to the default WorkerFactory.
You don't need a DelegatingWorkerFactory.
I need to pass several arguments as input for my WorkManager, but don't know how. Also, I want to pass Repository.
val data = workDataOf("cabinId" to task.cabinId)
val data2 = workDataOf("repository" to repository)
val uploadWorkRequest = OneTimeWorkRequestBuilder<WManager>()
.setInputData(data)
.setInputData(data2)
.build()
class WManager(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
override fun doWork(): Result {
val input = inputData.getInt("cabinId", 99)
val input2 = inputData("repository")
return Result.success(outputData)
}}
I tried to implement interface serializable for Repository but it does not work
You are doing the right thing. You can pass in multiple key value pairs to workDataOf.
So something like:
val data = workDataOf("cabinId" to task.cabinId, "repository" to repository)
val request = OneTimeWorkRequestBuilder<WManager>()
.setInputData(data)
.build()
WorkManager.getContext(context).enqueue(request)
This is assuming that repository is a serializable type. If not, you will need to do dependency injection.
Context
So, I've been working with the MVVM architecture just for a couple of projects. I'm still trying to figure out and improve how the architecture works. I always worked with the MVP architecture, using the usual toolset, Dagger for DI, usually multi-module projects, the Presenter layer being injected with a bunch of Interactors/UseCases, and each Interactor being injected with different Repositories to perform the backend API calls.
Now that I've moved into MVVM I changed the Presenter layer by the ViewModel, the communication from the ViewModel to the UI layer is being done through LiveData instead of using a View callback interface, and so on.
Looks like this:
class ProductDetailViewModel #inject constructor(
private val getProductsUseCase: GetProductsUseCase,
private val getUserInfoUseCase: GetUserInfoUseCase,
) : ViewModel(), GetProductsUseCase.Callback, GetUserInfoUseCase.Callback {
// Sealed class used to represent the state of the ViewModel
sealed class ProductDetailViewState {
data class UserInfoFetched(
val userInfo: UserInfo
) : ProductDetailViewState(),
data class ProductListFetched(
val products: List<Product>
) : ProductDetailViewState(),
object ErrorFetchingInfo : ProductDetailViewState()
object LoadingInfo : ProductDetailViewState()
}
...
// Live data to communicate back with the UI layer
val state = MutableLiveData<ProductDetailViewState>()
...
// region Implementation of the UseCases callbacks
override fun onSuccessfullyFetchedProducts(products: List<Product>) {
state.value = ProductDetailViewState.ProductListFetched(products)
}
override fun onErrorFetchingProducts(e: Exception) {
state.value = ProductDetailViewState.ErrorFetchingInfo
}
override fun onSuccessfullyFetchedUserInfo(userInfo: UserInfo) {
state.value = ProductDetailViewState.UserInfoFetched(userInfo)
}
override fun onErrorFetchingUserInfo(e: Exception) {
state.value = ProductDetailViewState.ErrorFetchingInfo
}
// Functions to call the UseCases from the UI layer
fun fetchUserProductInfo() {
state.value = ProductDetailViewState.LoadingInfo
getProductsUseCase.execute(this)
getUserInfoUseCase.execute(this)
}
}
There's no rocket science here, sometimes I change the implementation to use more than one LiveData property to keep track of the changes. By the way, this is just an example that I wrote on the fly, so don't expect it to compile. But It's just this, the ViewModel is injected with a bunch of UseCases, it implements the UseCases callback interfaces and when I get the results from the UseCases I communicate that to the UI layer through LiveData.
My UseCases usually look like this:
// UseCase interface
interface GetProductsUseCase {
interface Callback {
fun onSuccessfullyFetchedProducts(products: List<Product>)
fun onErrorFetchingProducts(e: Exception)
}
fun execute(callback: Callback)
}
// Actual implementation
class GetProductsUseCaseImpl(
private val productRepository: ApiProductRepostory
) : GetProductsUseCase {
override fun execute(callback: Callback) {
productRepository.fetchProducts() // Fetches the products from the backend through Retrofit
.subscribe(
{
// onNext()
callback.onSuccessfullyFetchedProducts(it)
},
{
// onError()
callback.onErrorFetchingProducts(it)
}
)
}
}
My Repository classes are usually wrappers for the Retrofit instance and they take care of setting the proper Scheduler so everything runs on the proper thread and mapping the backend responses into model classes. By backend responses I mean classes mapped with Gson (for example
a list of ApiProductResponse) and they get mapped into model classes (for example a List of Product which I use across the App)
Question
My question here is that since I started working with the MVVM architecture all the articles and all the examples, people is either injecting the Repositories right into the ViewModel (duplicating code to handle errors and mapping the responses) or either using the Single Source of Truth pattern (getting the information from Room using Room's Flowables). But I haven't seen anyone use UseCases with a ViewModel layer. I mean it's pretty handy, I get to keep things separated, I do the mapping of the backend responses within the UseCases, I handle any error there. But still, feels odds that I don't see anyone doing this, is there some way to improve the UseCases to make them more friendly to the ViewModels in terms of API? Perform the communication between the UseCases and the ViewModels with something else than a callback interface?
Please let me know if you need any more info about this. Sorry for the examples, I know that these are not the best, I just came out with something simple for sake of explaining it better.
Thanks,
Edit #1
This is how my Repository classes look like:
// ApiProductRepository interface
interface ApiProductRepository {
fun fetchProducts(): Single<NetworkResponse<List<ApiProductResponse>>>
}
// Actual implementation
class ApiProductRepositoryImpl(
private val retrofitApi: ApiProducts, // This is a Retrofit API interface
private val uiScheduler: Scheduler, // AndroidSchedulers.mainThread()
private val backgroundScheduler: Scheduler, // Schedulers.io()
) : GetProductsUseCase {
override fun fetchProducts(): Single<NetworkResponse<List<ApiProductResponse>>> {
return retrofitApi.fetchProducts() // Does the API call using the Retrofit interface. I've the RxAdapter set.
.wrapOnNetworkResponse() // Extended function that converts the Retrofit's Response object into a NetworkResponse class
.observeOn(uiScheduler)
.subscribeOn(backgroundScheduler)
}
}
// The network response class is a class that just carries the Retrofit's Response class status code
Update your use case so that it returns Single<List<Product>>:
class GetProducts #Inject constructor(private val repository: ApiProductRepository) {
operator fun invoke(): Single<List<Product>> {
return repository.fetchProducts()
}
}
Then, update your ViewModel so that it subscribes to the products stream:
class ProductDetailViewModel #Inject constructor(
private val getProducts: GetProducts
): ViewModel() {
val state: LiveData<ProductDetailViewState> get() = _state
private val _state = MutableLiveData<ProductDetailViewState>()
private val compositeDisposable = CompositeDisposable()
init {
subscribeToProducts()
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
private fun subscribeToProducts() {
getProducts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.main())
.subscribe(
{
// onNext()
_state.value = ProductListFetched(products = it)
},
{
// onError()
_state.value = ErrorFetchingInfo
}
).addTo(compositeDisposable)
}
}
sealed class ProductDetailViewState {
data class ProductListFetched(
val products: List<Product>
): ProductDetailViewState()
object ErrorFetchingInfo : ProductDetailViewState()
}
One thing I'm leaving out it is the adaptation of List<ApiProductResponse>> to List<Product> but that can be handled by mapping the list with a helper function.
I have just started using MVVM for the last 2 of my projects. I can share with you my process of dealing with REST APIs in ViewModel. Hope it will help you and others.
Make a Generic Retrofit Executer Class with their callbacks. which will take a retrofit call object and gives you data.
Make a repository for Your particular package or module where you can handle all API request. in my case, I am getting one user by its id from API.
Here is User Repository.
class UserRepository {
#Inject
lateinit var mRetrofit: Retrofit
init {
MainApplication.appComponent!!.inject(this)
}
private val userApi = mRetrofit.create(UserApi::class.java)
fun getUserbyId(id: Int): Single<NetworkResponse<User>> {
return Single.create<NetworkResponse<User>>{
emitter ->
val callbyId = userApi.getUserbyId(id)
GenericReqExecutor(callbyId).executeCallRequest(object : ExecutionListener<User>{
override fun onSuccess(response: User) {
emitter.onSuccess(NetworkResponse(success = true,
response = response
))
}
override fun onApiError(error: NetworkError) {
emitter.onSuccess(NetworkResponse(success = false,
response = User(),
networkError = error
))
}
override fun onFailure(error: Throwable) {
emitter.onError(error)
}
})
}
}
}
Then Use this Repository in your ViewModel. In my case here is my LoginViewModel code.
class LoginViewModel : ViewModel() {
var userRepo = UserRepository()
fun getUserById(id :Int){
var diposable = userRepo.getUserbyId(id).subscribe({
//OnNext
},{
//onError
})
}
}
I hope this approach can help you to reduce some of your boilerplate code.
Thanks
I had the same question when I started using MVVM a while ago. I came up with the following solution, based on Kotlin suspend functions and coroutines:
Change ApiProductRepositoryImpl.fetchProducts() to run synchronously. To do this, change your retrofit interface to return Call<...> and then change the repository implementation to
// error handling omitted for brevity
override fun fetchProducts() = retrofitApi.fetchProducts().execute().body()
Make your use cases implement the following interface:
interface UseCase<InputType, OutputType> {
suspend fun execute(input: InputType): OutputType
}
so your GetProductsUseCase would look like this:
class GetProductsUseCase: UseCase<Unit, List<Product>> {
suspend fun execute(input: Unit): List<Product> = withContext(Dispatchers.IO){
// withContext causes this block to run on a background thread
return#withContext productRepository.fetchProducts()
}
Execute the use case in your ViewModel
launch {
state.value = ProductDetailViewState.ProductListFetched(getProductsUseCase.execute())
}
See https://github.com/snellen/umvvm for more info and examples.
So from what I read, Dagger doesn't have support for inject in Worker yet. But there are some workarounds as people suggest. I have tried to do it a number of ways following examples online but none of them work for me.
When I don't try to inject anything into the Worker class, the code works fine, only that I can't do what I want because I need access to some DAOs and Services. If I use #Inject on those dependencies, the dependencies are either null or the worker never starts i.e the debugger doesn't even enter the Worker class.
For eg I tried doing this:
#Component(modules = {Module.class})
public interface Component{
void inject(MyWorker myWorker);
}
#Module
public class Module{
#Provides
public MyRepository getMyRepo(){
return new myRepository();
}
}
And in my worker
#Inject
MyRepository myRepo;
public MyWorker() {
DaggerAppComponent.builder().build().inject(this);
}
But then the execution never reaches the worker. If I remove the constructor, the myRepo dependency remains null.
I tried doing many other things but none work. Is there even a way to do this? Thanks!!
Overview
You need to look at WorkerFactory, available from 1.0.0-alpha09 onwards.
Previous workarounds relied on being able to create a Worker using the default 0-arg constructor, but as of 1.0.0-alpha10 that is no longer an option.
Example
Let's say that you have a Worker subclass called DataClearingWorker, and that this class needs a Foo from your Dagger graph.
class DataClearingWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
lateinit var foo: Foo
override fun doWork(): Result {
foo.doStuff()
return Result.SUCCESS
}
}
Now, you can't just instantiate one of those DataClearingWorker instances directly. So you need to define a WorkerFactory subclass that can create one of them for you; and not just create one, but also set your Foo field too.
class DaggerWorkerFactory(private val foo: Foo) : WorkerFactory() {
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
val workerKlass = Class.forName(workerClassName).asSubclass(Worker::class.java)
val constructor = workerKlass.getDeclaredConstructor(Context::class.java, WorkerParameters::class.java)
val instance = constructor.newInstance(appContext, workerParameters)
when (instance) {
is DataClearingWorker -> {
instance.foo = foo
}
// optionally, handle other workers
}
return instance
}
}
Finally, you need to create a DaggerWorkerFactory which has access to the Foo. You can do this in the normal Dagger way.
#Provides
#Singleton
fun workerFactory(foo: Foo): WorkerFactory {
return DaggerWorkerFactory(foo)
}
Disabling Default WorkManager Initialization
You'll also need to disable the default WorkManager initialization (which happens automatically) and initialize it manually.
How you do this depends on the version of androidx.work that you're using:
2.6.0 and onwards:
In AndroidManifest.xml, add:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="YOUR_APP_PACKAGE.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
Pre 2.6.0:
In AndroidManifest.xml, add:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="YOUR_APP_PACKAGE.workmanager-init"
android:enabled="false"
android:exported="false"
tools:replace="android:authorities" />
Be sure to replace YOUR_APP_PACKAGE with your actual app's package. The <provider block above goes inside your <application tag.. so it's a sibling of your Activities, Services etc...
In your Application subclass, (or somewhere else if you prefer), you can manually initialize WorkManager.
#Inject
lateinit var workerFactory: WorkerFactory
private fun configureWorkManager() {
val config = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
WorkManager.initialize(this, config)
}
2020/06 Update
Things become much easier with Hilt and Hilt for Jetpack.
With Hilt, all you have to do is
add annotation #HiltAndroidApp to your Application class
inject out-of-box HiltWorkerFactory in the field fo Application class
Implement interface Configuration.Provider and return the injected work factory in Step 2.
Now, change the annotation on the constructor of Worker from #Inject to #WorkerInject
class ExampleWorker #WorkerInject constructor(
#Assisted appContext: Context,
#Assisted workerParams: WorkerParameters,
someDependency: SomeDependency // your own dependency
) : Worker(appContext, workerParams) { ... }
That's it!
(also, don't forget to disable default work manager initialization)
===========
Old solution
As of version 1.0.0-beta01, here is an implementation of Dagger injection with WorkerFactory.
The concept is from this article: https://medium.com/#nlg.tuan.kiet/bb9f474bde37 and I just post my own implementation of it step by step(in Kotlin).
===========
What's this implementation trying to achieve is:
Every time you want to add a dependency to a worker, you put the dependency in the related worker class
===========
1. Add an interface for all worker's factory
IWorkerFactory.kt
interface IWorkerFactory<T : ListenableWorker> {
fun create(params: WorkerParameters): T
}
2. Add a simple Worker class with a Factory which implements IWorkerFactory and also with the dependency for this worker
HelloWorker.kt
class HelloWorker(
context: Context,
params: WorkerParameters,
private val apiService: ApiService // our dependency
): Worker(context, params) {
override fun doWork(): Result {
Log.d("HelloWorker", "doWork - fetchSomething")
return apiService.fetchSomething() // using Retrofit + RxJava
.map { Result.success() }
.onErrorReturnItem(Result.failure())
.blockingGet()
}
class Factory #Inject constructor(
private val context: Provider<Context>, // provide from AppModule
private val apiService: Provider<ApiService> // provide from NetworkModule
) : IWorkerFactory<HelloWorker> {
override fun create(params: WorkerParameters): HelloWorker {
return HelloWorker(context.get(), params, apiService.get())
}
}
}
3. Add a WorkerKey for Dagger's multi-binding
WorkerKey.kt
#MapKey
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
annotation class WorkerKey(val value: KClass<out ListenableWorker>)
4. Add a Dagger module for multi-binding worker (actually multi-binds the factory)
WorkerModule.kt
#Module
interface WorkerModule {
#Binds
#IntoMap
#WorkerKey(HelloWorker::class)
fun bindHelloWorker(factory: HelloWorker.Factory): IWorkerFactory<out ListenableWorker>
// every time you add a worker, add a binding here
}
5. Put the WorkerModule into AppComponent. Here I use dagger-android to construct the component class
AppComponent.kt
#Singleton
#Component(modules = [
AndroidSupportInjectionModule::class,
NetworkModule::class, // provides ApiService
AppModule::class, // provides context of application
WorkerModule::class // <- add WorkerModule here
])
interface AppComponent: AndroidInjector<App> {
#Component.Builder
abstract class Builder: AndroidInjector.Builder<App>()
}
6. Add a custom WorkerFactory to leverage the ability of creating worker since the release version of 1.0.0-alpha09
DaggerAwareWorkerFactory.kt
class DaggerAwareWorkerFactory #Inject constructor(
private val workerFactoryMap: Map<Class<out ListenableWorker>, #JvmSuppressWildcards Provider<IWorkerFactory<out ListenableWorker>>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val entry = workerFactoryMap.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
val factory = entry?.value
?: throw IllegalArgumentException("could not find worker: $workerClassName")
return factory.get().create(workerParameters)
}
}
7. In Application class, replace WorkerFactory with our custom one:
App.kt
class App: DaggerApplication() {
override fun onCreate() {
super.onCreate()
configureWorkManager()
}
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().create(this)
}
#Inject lateinit var daggerAwareWorkerFactory: DaggerAwareWorkerFactory
private fun configureWorkManager() {
val config = Configuration.Builder()
.setWorkerFactory(daggerAwareWorkerFactory)
.build()
WorkManager.initialize(this, config)
}
}
8. Don't forget to disable default work manager initialization
AndroidManifest.xml
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:enabled="false"
android:exported="false"
tools:replace="android:authorities" />
That's it.
Every time you want to add a dependency to a worker, you put the dependency in the related worker class (like HelloWorker here).
Every time you want to add a worker, implement the factory in the worker class and add the worker's factory to WorkerModule for multi-binding.
For more detail, like using AssistedInject to reduce boilerplate codes, please refer to the article I mentioned at beginning.
I use Dagger2 Multibindings to solve this problem.
The similar approach is used to inject ViewModel objects (it's described well here). Important difference from view model case is the presence of Context and WorkerParameters arguments in Worker constructor. To provide these arguments to worker constructor intermediate dagger component should be used.
Annotate your Worker's constructor with #Inject and provide your desired dependency as constructor argument.
class HardWorker #Inject constructor(context: Context,
workerParams: WorkerParameters,
private val someDependency: SomeDependency)
: Worker(context, workerParams) {
override fun doWork(): Result {
// do some work with use of someDependency
return Result.SUCCESS
}
}
Create custom annotation that specifies the key for worker multibound map entry.
#MustBeDocumented
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class WorkerKey(val value: KClass<out Worker>)
Define worker binding.
#Module
interface HardWorkerModule {
#Binds
#IntoMap
#WorkerKey(HardWorker::class)
fun bindHardWorker(worker: HardWorker): Worker
}
Define intermediate component along with its builder. The component must have the method to get workers map from dependency graph and contain worker binding module among its modules. Also the component must be declared as a subcomponent of its parent component and parent component must have the method to get the child component's builder.
typealias WorkerMap = MutableMap<Class<out Worker>, Provider<Worker>>
#Subcomponent(modules = [HardWorkerModule::class])
interface WorkerFactoryComponent {
fun workers(): WorkerMap
#Subcomponent.Builder
interface Builder {
#BindsInstance
fun setParameters(params: WorkerParameters): Builder
#BindsInstance
fun setContext(context: Context): Builder
fun build(): WorkerFactoryComponent
}
}
// parent component
#ParentComponentScope
#Component(modules = [
//, ...
])
interface ParentComponent {
// ...
fun workerFactoryComponent(): WorkerFactoryComponent.Builder
}
Implement WorkerFactory. It will create the intermediate component, get workers map, find the corresponding worker provider and construct the requested worker.
class DIWorkerFactory(private val parentComponent: ParentComponent) : WorkerFactory() {
private fun createWorker(workerClassName: String, workers: WorkerMap): ListenableWorker? = try {
val workerClass = Class.forName(workerClassName).asSubclass(Worker::class.java)
var provider = workers[workerClass]
if (provider == null) {
for ((key, value) in workers) {
if (workerClass.isAssignableFrom(key)) {
provider = value
break
}
}
}
if (provider == null)
throw IllegalArgumentException("no provider found")
provider.get()
} catch (th: Throwable) {
// log
null
}
override fun createWorker(appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters) = parentComponent
.workerFactoryComponent()
.setContext(appContext)
.setParameters(workerParameters)
.build()
.workers()
.let { createWorker(workerClassName, it) }
}
Initialize a WorkManager manually with custom worker factory (it must be done only once per process). Don't forget to disable auto initialization in manifest.
manifest:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
Application onCreate:
val configuration = Configuration.Builder()
.setWorkerFactory(DIWorkerFactory(parentComponent))
.build()
WorkManager.initialize(context, configuration)
Use worker
val request = OneTimeWorkRequest.Builder(workerClass).build(HardWorker::class.java)
WorkManager.getInstance().enqueue(request)
Watch this talk for more information on WorkManager features.
In WorkManager alpha09 there is a new WorkerFactory that you can use to initialize the Worker the way you want to.
Use the new Worker constructor which takes in an ApplicationContext and WorkerParams.
Register an implementation of WorkerFactory via Configuration.
Create a configuration and register the newly created WorkerFactory.
Initialize WorkManager with this configuration (while removing the ContentProvider which initializes WorkManager on your behalf).
You need to do the following:
public DaggerWorkerFactory implements WorkerFactory {
#Nullable Worker createWorker(
#NonNull Context appContext,
#NonNull String workerClassName,
#NonNull WorkerParameters workerParameters) {
try {
Class<? extends Worker> workerKlass = Class.forName(workerClassName).asSubclass(Worker.class);
Constructor<? extends Worker> constructor =
workerKlass.getDeclaredConstructor(Context.class, WorkerParameters.class);
// This assumes that you are not using the no argument constructor
// and using the variant of the constructor that takes in an ApplicationContext
// and WorkerParameters. Use the new constructor to #Inject dependencies.
Worker instance = constructor.newInstance(appContext,workerParameters);
return instance;
} catch (Throwable exeption) {
Log.e("DaggerWorkerFactory", "Could not instantiate " + workerClassName, e);
// exception handling
return null;
}
}
}
// Create a configuration
Configuration configuration = new Configuration.Builder()
.setWorkerFactory(new DaggerWorkerFactory())
.build();
// Initialize WorkManager
WorkManager.initialize(context, configuration);