Dagger 2, um ano depois
Bibliotecas que ajudam a organizar melhor o código que produzimos são sempre bem vindas, e já faz mais de um ano que comecei a estudar o Dagger 2.
Se você nunca mexeu com Dagger 2, talvez valha a pena dar uma olhada em alguns materiais antes de começar a ler este post. Um bom começo é a apresentação que fiz no TDC SP 2015, que aborda o assunto desde o início, trazendo várias referências relevantes no final :)
Cerca de 15 meses após o lançamento de sua primeira versão estável, como a biblioteca evoluiu? O que tem de novo nesse meio tempo até a versão 2.5, lançada mês passado?
@Reusable
Na versão 2.0 do Dagger já tínhamos um escopo padrão, o @Singleton, herdado da especificação JSR 330, para objetos que deveriam possuir apenas uma única instância ao longo de toda a aplicação. Na versão 2.3 do Dagger, temos a introdução da anotação de escopo @Reusable, que nos fornece uma instância ativa de cada vez, podendo ou não ser reutilizada. Porém, ao contrário do @Singleton, não há garantia desse reuso. Esta anotação é interessante, por exemplo, para classes Helper ou Utils, ou qualquer outro tipo de classe stateless.
A utilização do @Reusable é feita no próprio método anotado com @Provides:
@Module
public class DaggerModule {
@Provides
@Reusable
public TextHelper provideTextHelper(Context context) {
return new TextHelper(context);
}
}
@Binds
Pra quem foi early adopter do Dagger 2 lembra que, quando queríamos trabalhar com interfaces e, ao mesmo tempo, separar a complexidade de criação de alguns objetos (interface e sua classe concreta, por exemplo), nossos módulos não ficavam exatamente elegantes. Até então, tínhamos que fazer algumas coisas como:
@Module
public class BluetoothModule {
@Provides
public BluetoothImpl provideBluetoothImpl() {
return new BluetoothImpl();
}
@Provides
public Bluetooth provideBluetooth(BluetoothImpl impl) {
return impl;
}
}
A partir da versão 2.4, temos a anotação @Binds, que permite que tenhamos módulos abstratos, onde fazemos apenas o bind da implementação e da abstração. Assim, podemos separar melhor a implementação dos módulos, além de possibilitar que o próprio Dagger otimize esse processo, conforme sugere a documentação.
@Module
public class ConcreteModule {
@Provides
public BluetoothImpl provideConcreteImpl() {
return new BluetoothImpl();
}
}@Module
public abstract class AbstractModule {
@Binds
public abstract Bluetooth bindBluetooth(BluetoothImpl impl);
}
Multibinding
Apesar de existir de forma mais primitiva na versão 2.0, a possibilidade de fazer multibindings e devolver objetos na forma de coleções (sejam elas em Map ou Set) evoluiu muito nas releases seguintes. A própria documentação ilustra diversos cenários e usos para essa funcionalidade.
Para realizar multibindings com Sets, basta declarar anotar os métodos @Provides com @IntoSet (caso o método esteja adicionando apenas um valor a coleção), ou @ElementsIntoSet (caso o método esteja adicionando mais de um valor a coleção).
@Module
public class DaggerModule {
@Provides
@IntoSet
public String provideValueA() {
return "ABC";
}
@Provides
@IntoSet
public String provideValueB() {
return "DEF";
}
@Provides
@ElementsIntoSet
public Set<String> provideMultipleItems() {
return new HashSet<>(Arrays.asList("GHI", "JKL"));
}
}
A implementação de uma Activity ficaria assim:
public class MainActivity extends AppCompatActivity {
@Inject
Set<String> values;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DaggerApp app = (DaggerApp) getApplication();
app.getComponent().inject(this);
// ABC, DEF, GHI e JKL estão presentes no Set
for (String v : values) {
Log.i("SET", v);
}
}
}
Para Maps, o método também deve ser anotado com @IntKey, @StringKey ou @ClassKey, para que a dependência seja inserida corretamente e possa ser acessada pelo objeto injetado.
@Module
public class DaggerModule {
@Provides
@IntoMap
@IntKey(1)
public String provideValue() {
return "ABC";
}
}
E a classe a ser injetada:
public class MainActivity extends AppCompatActivity {
@Inject
Map<Integer, String> values;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DaggerApp app = (DaggerApp) getApplication();
app.getComponent().inject(this);
for (Integer key : values.keySet()) {
Log.i("MAP", values.get(key));
}
}
}
Caso determinado multibinding possa ser uma coleção vazia, é necessário criar um módulo abstrato e criar um método declarando tal coleção, anotando-o com @Multibinds:
@Module
public abstract class AbstractModule {
@Multibinds
public abstract Set<String> providePossiblyEmptySet();
}
Um artigo bem interessante sobre multibindings, do Miroslaw Stanek, onde ele utiliza Dagger, multibindings e Auto Factory para criar uma prova de conceito e injetar diferentes ViewHolders. Vale a leitura!
Producers
Também presente na versão 2.0, mas tendo evoluído e sido otimizado consideravelmente desde a primeira versão, os Producers são uma forma de injeção de dependências assíncrona.
Porém, nesse caso, temos as desvantagens de adicionar a dependência do Guava (o que pode aumentar consideravelmente a quantidade de métodos no APK, caso o Proguard não esteja sendo utilizado) e a verbosidade da solução, que não se baseia na especificação JSR 330.
Para o uso dos Producers, primeiramente temos que adicionar a dependência específica no nosso arquivo build.gradle:
dependencies {
...
compile 'com.google.dagger:dagger:2.5'
compile 'com.google.dagger:dagger-producers:2.5'
apt 'com.google.dagger:dagger-compiler:2.5'
}
Em seguida precisamos criar um módulo que indica como as dependências assíncronas serão entregues, retornando um objeto Executor:
@Module
public class ExecutorModule {
@Provides
@Production
public Executor executor() {
return Executors.newCachedThreadPool();
}
}
As nossas dependências (a serem entregues de forma assíncronas) devem ser declaradas em um @ProducerModule, muito similar aos módulos já utilizados. Já os métodos, devem ser anotados com @Produces — por padrão, todas as dependências são singleton.
@ProducerModule
public class MyProducerModule {
@Produces
public LayoutInflater produceLayoutInflater(Context context) {
return LayoutInflater.from(context);
}
}
Já o componente precisa ser um @ProductionComponent, incluindo o módulo de execução e os Producer Modules. As dependências que serão injetadas são declaradas em métodos retornando um ListenableFuture, assim como no exemplo:
@ProductionComponent(
modules = {MyProducerModule.class, ExecutorModule.class},
dependencies = DaggerComponent.class
)
public interface MyProducerComponent {
ListenableFuture<LayoutInflater> layoutInflater();
}
Os Production Components podem depender de componentes e módulos convencionais.
Por fim, uma das formas de se obter a dependência é através do método Futures.addCallback() do Guava:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DaggerApp app = (DaggerApp) getApplication();
Futures.addCallback(app.getProducerComponent()
.layoutInflater(),
new FutureCallback<LayoutInflater>() {
@Override
public void onSuccess(LayoutInflater result) {
// result is not null! :)
}
@Override
public void onFailure(Throwable t) {
Log.e("Producers", "Failed to get dependency");
}
});
}
}
Novamente, o artigo do Stanek ilustra o uso para os Producers mais a fundo, indicados para dependências pesadas e custosas — o que, teoricamente, deve ser evitado em um aplicativo Android :)
Conclusão
Posso dizer que a evolução do Dagger tem acontecido de forma bastante satisfatória, principalmente com um ciclo de releases mais constantes desde o início deste ano. Além disso, se antes havia algum receio do uso da biblioteca, posso dizer que alguns projetos em produção com a biblioteca já rodam tranquilamente há meses sem quaisquer problemas relacionados ao uso da biblioteca.
Então, façam bom uso e viva o open source! :)