Configuração do Dagger 2 com WorkManager

Tempo de leitura: 6 minutes

TL; DR

Não podemos realizar a injeção de construtor na classe Worker por causa dos parâmetros e appContext só está disponível em tempo de execução, portanto, executamos a injeção de construtor com seu Factory. Em seguida, configure um mapa de multi binds com essas fábricas, injete este mapa de Factory dentro de uma WorkerFactory personalizada e crie uma nova instância de Worker a partir daí.

Acho que é um bom momento para escrever sobre isso, mais especificamente, como injetar dependência na classe Worker?

Neste tutorial, não estamos discutindo a base do WorkManager, em vez de uma configuração adequada do Dagger 2 com ele. Portanto, se você é novo no WorkManager, recomendo verificar o documento oficial

 

Objetivo

No final deste tutorial, não apenas você pode executar injeção de construtor na classe Worker, mas também compreender totalmente como as coisas funcionam.

class HelloWorldWorker @Inject constructor(
    private val appContext: Context,
    private val params: WorkerParameters,
    private val foo: Foo // dependência de teste
    // adicione mais dependências aqui
) : Worker(appContext, params)

Problemas

No trecho de código acima, o primeiro problema é que o Worker é instanciado pelo WorkerManager (como Activity e Fragment são instanciados pelo framework Android) e não por nós. Isso significa que você não pode passar nenhum outro parâmetro, pois as dependências no construtor esperam os parâmetros Context e WorkerParameters, portanto, é quase impossível realizar injeção de construtor na classe Worker. Isso deixou para nós a única opção é a injeção em campo.

class HelloWorldWorker(
    appContext: Context,
    params: WorkerParameters
) : Worker(appContext, params) {

    @Inject lateinit var foo: Foo

    override fun doWork(): Result {
        TODO()
    }
}

Ninguém gosta de injeção em campo, de uma forma ou de outra, exige que a classe conheça seu injetor.

 

Solução

Na versão alpha 9, a equipe do Android apresenta uma nova classe abstrata chamada WorkerFactory

“Um objeto de fábrica que cria instâncias de ListenableWorker. A fábrica é invocada sempre que uma obra é executada”

Resumindo, se houver uma fábrica personalizada registrada no WorkManager (vamos chamá-la de SampleWorkerFactory), toda vez que um novo trabalhador for solicitado, o WorkManager solicitará a SampleWorkerFactory para construir uma nova instância de trabalhador. Isso é ótimo porque, por meio de nossa fábrica personalizada, agora podemos decidir como construir a instância do Worker, não mais restrita ao construtor padrão.

“TL; DR com a introdução de WorkerFactory, agora podemos realizar injeção de construtor em nosso trabalhador.”

 

Implementação passo a passo

Voltaremos ao SampleWorkerFactory mais tarde, vamos nos concentrar na implementação abaixo:

interface ChildWorkerFactory {
    fun create(appContext: Context, params: WorkerParameters): ListenableWorker
}
class Foo @Inject constructor() // test dependence

class HelloWorldWorker(
    private val appContext: Context,
    private val params: WorkerParameters,
    private val foo: Foo // test dependence
    // adicione mais dependências aqui
) : Worker(appContext, params) {
    override fun doWork(): Result {
      TODO()
    }
  
    class Factory @Inject constructor(
        private val foo: Provider<Foo>
    ) : ChildWorkerFactory {
        override fun create(appContext: Context, params: WorkerParameters): ListenableWorker {
            return HelloWorldWorker(
                appContext,
                params,
                foo.get()
            )
        }
    }
}

HelloWorldWorker precisa de Foo como uma dependência. Não podemos fazer uma injeção de construtor em Foo conforme mencionado. Portanto, executamos a injeção de construtor em sua classe interna HelloWorldWorker.Factory. A classe de fábrica pode ter qualquer nome, desde que implemente ChildWorkerFactory.

Por que precisamos da interface ChildWorkerFactory? Essa interface, mais tarde, se torna útil, pois nossa configuração invoca o Dagger multi-bind (configuração semelhante com ViewModel em google/iosched).

“Nota: agora temos 2 tipos de fábrica:

WorkerFactory: uma fábrica personalizada que registramos no WorkManager, nossa implementação é SampleWorkerFactory

ChildWorkerFactory: uma fábrica para cada Worker que oferece suporte para o propósito de injeção de construtor (HelloWorldWorker.Factory)”

Agora, todas as implementações ChildWorkerFactory estão disponíveis no gráfico de dependência. Agora vinculamos essas ChildWorkerFactory em um mapa multi-bind.

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

@Module
interface WorkerBindingModule {
    @Binds
    @IntoMap
    @WorkerKey(HelloWorldWorker::class)
    fun bindHelloWorldWorker(factory: HelloWorldWorker.Factory): ChildWorkerFactory
}

@Component(
    modules = [
        WorkerBindingModule::class,
    ]
)
interface SampleComponent {
    // other method
}

Em seguida, injete esse mapa em nossa WorkerFactory personalizada, também conhecida como SampleWorkerFactory – a fábrica personalizada que registraremos com WorkerManager. Você não me ouviu mal, fábricas dentro de uma fábrica.

Lembre-se de vincular SampleWorkerFactory a WorkerFactory também.

class SampleWorkerFactory @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) }
        val factoryProvider = foundEntry?.value
            ?: throw IllegalArgumentException("unknown worker class name: $workerClassName")
        return factoryProvider.get().create(appContext, workerParameters)
    }
}

Uma etapa importante final, registre SampleWorkerFactory no WorkManager, um em nosso aplicativo e um em AndroidManifest.xml

class SampleApplication : DaggerApplication() {
    @Inject lateinit var workerFactory: WorkerFactory
    
    override fun onCreate() {
        super.onCreate()
        // register ours custom factory to WorkerManager
        WorkManager.initialize(this, Configuration.Builder().setWorkerFactory(factory).build())
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.sample.daggerworkmanagersample">
    
   // Observação: o conteúdo foi reduzido, demonstre como registrar apenas o WorkerFactory personalizado
    
    <application android:name=".SampleApplication">

        <provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:authorities="${applicationId}.workmanager-init"
            android:exported="false"
            tools:node="remove"/>
    
    </application>
</manifest>

Em seguida, clicamos no botão Executar …

D/HelloWorldWorker: Hello world!
D/HelloWorldWorker: Injected foo: com.sample.daggerworkmanagersample.Foo@215b58d0
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=c1628749-ed19-4b11-b027-95031d3b3bae, tags={ com.sample.daggerworkmanagersample.HelloWorldWorker } ]

Funciona! mas…

O problema não é parar aí. Apesar do padrão de fábrica duplo funcionar muito bem, a pior parte é que temos que implementar cada ChildWorkerFactory manualmente, este exemplo tem apenas 1 dependência (Foo) imagine que temos 10 dependências para cada Worker e nosso aplicativo tem um total de 10 Worker? Tecnicamente ainda podemos fazer isso, mas você sabe como é, não pode acabar bem para nós.

É aqui que AssistedInject entra em jogo. Uma biblioteca da Square compatível com Dagger 2. Possui 2 jobs

  • Gera todas as implementações ChildWorkerFactory
  • Vincule essas implementações ao gráfico de dependência por meio de um módulo (mais detalhes posteriormente)

Portanto, ele gera 2 factories e um módulo que inclui todas as ligações de implementação

Configurar nossa base de código existente com AssistedInject é simples. Anote a classe Worker com @AssistedInject. Quaisquer parâmetros que desejamos criar com a fábrica gerada, anote-os com @Assisted. E para a fábrica (a classe original era que agora se tornou interface), anote-a com @AssistedInject.Factory. Nossas classes Worker agora parecem um monte de produtos de limpeza e a parte mais divertida é que agora não precisamos mais escrever esses códigos padronizados de fábrica.

class HelloWorldWorker @AssistedInject constructor(
    @Assisted private val appContext: Context,
    @Assisted private val params: WorkerParameters,
    private val foo: Foo
) : Worker(appContext, params) {
    private val TAG = "HelloWorldWorker"
    override fun doWork(): Result {
        Log.d(TAG, "Olá Mundo!")
        Log.d(TAG, "Foo injetado: $foo")
        return Result.success()
    }

    @AssistedInject.Factory
    interface Factory : ChildWorkerFactory
}

Uma vez que AssistedInject vincula essas fábricas dentro de um módulo. Declare um módulo que inclui o módulo gerado, anote-o com @AssistedModule, adicione-o ao nosso Componente. Isso torna todas as implementações ChildWorkerFactory disponíveis em nosso gráfico de dependência de componente.

@Module(includes = [AssistedInject_SampleAssistedInjectModule::class])
@AssistedModule
interface SampleAssistedInjectModule

@Component(
    modules = [
        SampleAssistedInjectModule::class,
        WorkerBindingModule::class
    ]
)
interface SampleComponent {
    // setup
}

Apertamos o botão de diversão novamente e tudo está funcionando como esperado.

 

Entenda o código gerado

é sempre bom quando podemos entender o que está acontecendo nos bastidores, não apenas confiar na “mágica”, então vamos mergulhar no código-fonte gerado.

public final class HelloWorldWorker_AssistedFactory implements HelloWorldWorker.Factory {
  private final Provider<Foo> foo;

  @Inject
  public HelloWorldWorker_AssistedFactory(Provider<Foo> foo) {
    this.foo = foo;
  }

  @Override
  public ListenableWorker create(Context appContext, WorkerParameters params) {
    return new HelloWorldWorker(
        appContext,
        params,
        foo.get());
  }
}

@Module
public abstract class AssistedInject_SampleAssistedInjectModule {
  private AssistedInject_SampleAssistedInjectModule() {
  }

  @Binds
  abstract HelloWorldWorker.Factory bind_com_sample_daggerworkmanagersample_HelloWorldWorker(
      HelloWorldWorker_AssistedFactory factory);
}

Em primeiro lugar, a implementação gerada de HelloWorldWorker.Factory parece quase igual ao nosso código original. Em seguida, o módulo gerado (a.k.a AssistedInject_SampleAssistedInjectModule) AssistedInject simplesmente vincula HelloWorldWorker_AssistedFactory a HelloWorldWorker.Factory.

 

Resumo

Nós quando de:

  • Não é possível controlar como a instância do Worker é instanciada
    => registrar um WorkerFactory personalizado para WorkerManager
  • Não é possível executar injeção de construtor na classe Worker
    => usar padrão de fábrica
  • Não quero implementar essas fábricas manualmente
    => usar AssistedInject

O resultado final: podemos realizar uma injeção de construtor em uma classe Worker, o código de injeção é gerado por AsssistedInject em vez de Dagger, o processo é automatizado. A parte mais importante, seguimos um princípio básico de injeção de dependência:

uma classe não deve saber nada sobre como é injetado.

 

 

Configurações alternativas

Anteriormente, há muitas discussões quando esse problema surge. Para resumir, existem 2 soluções possíveis para isso. Todos estão funcionando conforme o esperado, você nem consegue perceber a diferença de desempenho. Um deles ainda tem uma vantagem clara sobre esta configuração. Portanto, vale a pena mencioná-los, deixando a decisão para você.

 

SubComponent

Como mencionado, appContext e instância de params estão disponíveis apenas em tempo de execução, assim como o argumento Activity, Fragment e Fragment´s. Por causa disso, podemos fazer a mesma abordagem com Activity, Fragment etc. Leia mais sobre isso aqui.

Vantagem: nenhuma terceira chamada de biblioteca (AssistedInject)

 

Member injection

Provavelmente a configuração mais fácil, uma vez que invoca apenas uma declaração de método de injeção dentro de nosso componente e uma classe de trabalho base que executa a busca de componente e injeta. Sem AssistedInject, sem dagger-android, sem multi-binding, sem ChildWorkerFactory, sem WorkerFactory. Tudo bagunçado, apenas um método e uma classe base e você está pronto para ir.

Vantagem: super fácil de configurar, funciona melhor para novos desenvolvedores.

Então, qual é a vantagem da minha configuração? por que precisamos despender tanto esforço nesta configuração enorme? A resposta, meu amigo, é TESTAR. Como tudo é injetado por meio do construtor, podemos facilmente simular nossa dependência para testar a implementação do Worker.

 

Referência

https://google.github.io/dagger/android

https://github.com/google/guice/wiki/AssistedInject

https://jakewharton.com/helping-dagger-help-you/