web-dev-qa-db-fra.com

Dague2: impossible d'injecter des dépendances dans WorkManager

Donc, d'après ce que j'ai lu, Dagger ne supporte pas encore l'injection dans Worker. Mais il existe certaines solutions de rechange, comme le suggèrent les gens. J'ai essayé de le faire de différentes manières en suivant des exemples en ligne, mais aucun ne fonctionne pour moi. 

Lorsque je n'essaie pas d'injecter quoi que ce soit dans la classe Worker, le code fonctionne bien, sauf que je ne peux pas faire ce que je veux parce que j'ai besoin d'un accès à certains DAO et services. Si j'utilise @Inject sur ces dépendances, les dépendances sont nulles ou le travailleur ne démarre jamais car le débogueur n'entre même pas dans la classe Worker.

Par exemple, j'ai essayé de faire ceci:

@Component(modules = {Module.class})
public interface Component{

    void inject(MyWorker myWorker);
}

@Module
public class Module{

    @Provides
    public MyRepository getMyRepo(){
        return new myRepository();
    }

}

Et dans mon ouvrier

@Inject
MyRepository myRepo;

public MyWorker() {
    DaggerAppComponent.builder().build().inject(this);
}

Mais alors l'exécution n'atteint jamais le travailleur. Si je supprime le constructeur, la dépendance myRepo reste nulle.

J'ai essayé de faire beaucoup d'autres choses, mais aucune ne fonctionne. Y a-t-il même un moyen de faire ça? Merci!!

8
varunkr

Vue d'ensemble

Vous devez regarder WorkerFactory , disponible à partir de 1.0.0-alpha09.

Les solutions précédentes contenaient la possibilité de créer une variable Worker à l'aide du constructeur par défaut 0-arg, mais à partir de 1.0.0-alpha10, cette option n'est plus possible.

Exemple

Supposons que vous avez une sous-classe Worker appelée DataClearingWorker et que cette classe a besoin d'une Foo de votre graphe Dagger.

class DataClearingWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

    lateinit var foo: Foo

    override fun doWork(): Result {
        foo.doStuff()
        return Result.SUCCESS
    }
}

Maintenant, vous ne pouvez pas simplement instancier directement l’une de ces instances DataClearingWorker. Vous devez donc définir une sous-classe WorkerFactory pouvant en créer une pour vous. et non seulement en créer un, mais aussi définir votre champ Foo.

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
    }
}

Enfin, vous devez créer une DaggerWorkerFactory qui a accès à la Foo. Vous pouvez le faire de la manière normale Dague.

@Provides
@Singleton
fun workerFactory(foo: Foo): WorkerFactory {
    return DaggerWorkerFactory(foo)
}

Désactivation de l'initialisation par défaut de WorkManager

Vous devrez également désactiver l'initialisation par défaut WorkManager (qui se produit automatiquement) et l'initialiser manuellement.

Dans le AndroidManifest.xml, vous pouvez le désactiver comme ceci:

 <provider
        Android:name="androidx.work.impl.WorkManagerInitializer"
        Android:authorities="com.your.app.package.workmanager-init"
        Android:enabled="false"
        Android:exported="false"
        tools:replace="Android:authorities" />

Assurez-vous de remplacer com.votre.app.package par le package de votre application actuelle. Le bloc <provider ci-dessus va dans votre balise <application .. il s'agit donc d'un frère de votre Activities, Services etc ...

Dans votre sous-classe Application (ou ailleurs si vous préférez), vous pouvez initialiser manuellement WorkManager.

@Inject
lateinit var workerFactory: WorkerFactory

private fun configureWorkManager() {
    val config = Configuration.Builder()
        .setWorkerFactory(workerFactory)
        .build()

    WorkManager.initialize(this, config)
}
17
Craig Russell

J'utilise Dagger2 Multibindings pour résoudre ce problème. 

Une approche similaire est utilisée pour injecter des objets ViewModel (elle est bien décrite ici ). Une différence importante par rapport au cas de modèle de vue est la présence d'arguments Context et WorkerParameters dans le constructeur Worker. Pour fournir ces arguments au composant de poignard intermédiaire du constructeur du travailleur, il doit être utilisé.

  1. Annotez le constructeur de votre Worker avec @Inject et indiquez la dépendance souhaitée en tant qu'argument du constructeur.

    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
        }
    }
    
  2. Créez des annotations personnalisées qui spécifient la clé de l'entrée de la carte multibound de l'agent.

    @MustBeDocumented
    @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
    @Retention(AnnotationRetention.RUNTIME)
    @MapKey
    annotation class WorkerKey(val value: KClass<out Worker>)
    
  3. Définir la liaison du travailleur.

    @Module
    interface HardWorkerModule {
    
        @Binds
        @IntoMap
        @WorkerKey(HardWorker::class)
        fun bindHardWorker(worker: HardWorker): Worker
    }
    
  4. Définissez le composant intermédiaire avec son générateur et son module qui fourniront les objets Context et WorkerParameters. Le composant doit avoir la méthode permettant de mapper les travailleurs à partir du graphe de dépendance et de contenir le module de liaison de travail parmi ses modules. De plus, le composant doit être déclaré en tant que sous-composant de son composant parent et le composant parent doit avoir la méthode pour obtenir le générateur du composant enfant.

    @Module
    class ArgumentsModule(private val appContext: Context,
                          private val workerParameters: WorkerParameters) {
    
        @Provides
        fun provideAppContext() = appContext
    
        @Provides
        fun provideWorkerParameters() = workerParameters
    }
    
    typealias WorkerMap = MutableMap<Class<out Worker>, Provider<Worker>>
    
    @Subcomponent(modules = [
        ArgumentsModule::class,
        HardWorkerModule::class])
    interface WorkerFactoryComponent {
    
        fun workers(): WorkerMap
    
        @Subcomponent.Builder
        interface Builder {
            fun argumentsModule(module: ArgumentsModule): Builder
            fun build(): WorkerFactoryComponent
        }
    }
    
    // some module of the parent component
    @Module(subcomponents = [WorkerFactoryComponent::class
                //, ...
            ])
    class ParentComponentModule {
        // ...
    }
    
    // parent component
    @ParentComponentScope
    @Component(modules = [ParentComponentModule::class
                //, ...
            ])
    interface ParentComponent {
    
        // ...
    
        fun workerFactoryComponent(): WorkerFactoryComponent.Builder
    }
    
  5. Implémentez WorkerFactory. Il créera le composant intermédiaire, obtiendra la carte des travailleurs, trouvera le fournisseur de travailleurs correspondant et construira le travailleur demandé.

    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()
                .argumentsModule(ArgumentsModule(appContext, workerParameters))
                .build()
                .run { createWorker(workerClassName, workers()) }
    }
    
  6. Initialisez manuellement une WorkManager avec une fabrique de travailleurs personnalisée (cette opération ne doit être effectuée qu'une fois par processus). N'oubliez pas de désactiver l'initialisation automatique dans le manifeste.

manifeste:

    <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)
  1. Utiliser un travailleur

    val request = OneTimeWorkRequest.Builder(workerClass).build(HardWorker::class.Java)
    WorkManager.getInstance().enqueue(request)
    

Regardez ceci talk pour plus d’informations sur les fonctionnalités de WorkManager.

3
art

WorkManager alpha09 contient un nouveau WorkerFactory que vous pouvez utiliser pour initialiser la Worker comme vous le souhaitez.

  • Utilisez le nouveau constructeur Worker qui prend en compte ApplicationContext et WorkerParams.
  • Enregistrez une implémentation de WorkerFactory via Configuration.
  • Créez une configuration et enregistrez la WorkerFactory nouvellement créée.
  • Initialisez WorkManager avec cette configuration (en supprimant la ContentProvider qui initialise WorkManager en votre nom).

Vous devez faire ce qui suit:

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);
3
Rahul

A partir de la version 1.0.0-beta01, voici une implémentation de l'injection de poignard avec WorkerFactory.

Le concept est tiré de cet article: https://medium.com/@nlg.tuan.kiet/bb9f474bde37 et je viens de poster ma propre implémentation étape par étape (dans Kotlin ).

===========

Qu'est-ce que cette mise en œuvre essayant de réaliser est:

Chaque fois que vous souhaitez ajouter une dépendance à un agent, vous la placez dans la classe d'agent associée.

===========

1. Ajouter une interface pour l'usine de tous les travailleurs

IWorkerFactory.kt

interface IWorkerFactory<T : ListenableWorker> {
    fun create(params: WorkerParameters): T
}

2. Ajoute une simple classe Worker à une fabrique qui implémente IWorkerFactory et à la dépendance de ce travailleur.

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. Ajouter une WorkerKey pour le multi-binding de Dagger

WorkerKey.kt

@MapKey
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class WorkerKey(val value: KClass<out ListenableWorker>)

4. Ajoute un module de poignard pour le ouvrier à liaisons multiples (en réalité, associe l'usine à des liaisons multiples)

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. Placez le WorkerModule dans AppComponent. Ici, j'utilise dagger-Android pour construire la classe de composants

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. Ajoutez un utilisateur personnalisé WorkerFactory pour tirer parti de la possibilité de créer un employé depuis la version 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. Dans la classe Application, replace WorkerFactory par notre classe personnalisée:

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. N'oubliez pas de désactiver l'initialisation par défaut du gestionnaire de travaux

AndroidManifest.xml

<provider
    Android:name="androidx.work.impl.WorkManagerInitializer"
    Android:authorities="${applicationId}.workmanager-init"
    Android:enabled="false"
    Android:exported="false"
    tools:replace="Android:authorities" />

C'est tout.

Chaque fois que vous souhaitez ajouter une dépendance à un travailleur, vous la placez dans la classe de travail associée (comme ici, HelloWorker).

Chaque fois que vous souhaitez ajouter un travailleur, implémentez la fabrique dans sa classe et ajoutez la fabrique du travailleur à WorkerModule pour la liaison multiple.

Pour plus de détails, comme l'utilisation d'AssistedInject pour réduire les codes standard, veuillez vous reporter à l'article que j'ai mentionné au début.

2
diousk