Configuração do Dagger 2 com WorkManager
Conteudo
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/