Desmistificando o Dagger 2

Sem dúvidas, uma das bibliotecas mais famosas da “atualidade” (embora tenha vários anos de vida) no universo Android, mas que mesmo assim nos causa muita confusão e dúvida.

Até mesmo para engenheiros experientes, o Dagger 2 causa um pouco de “medo” para implementar.

Mas a parte boa é que ele é muito simples! Vamos juntos tentar dissecar o problema até que você tenha o famoso Eureka!

Inversion of Control e Dependency Inversion

Para entender bem o Dagger 2, precisamos entender o que significa DI e IoC.

Vamos criar uma classe qualquer:

public class NossaClasse {
    private NossasDependencia nossaDependencia;

    public NossaClasse (){
          nossaDependencia = new nossaDependencia();
    }

    public String retornaAlgumAtributo(){
        nossaDependencia.retornaAlgumAtributo();
    }
}

Agora, como vamos testar essa classe? Perceba que não temos como "controlar" a NossaDependencia, já que a responsabilidade de criar está nas mãos da NossaClasse.

Para resolver isso, podemos remover a responsabilidade da NossaClasse de criar , utilizando o princípio do IoC:

public class NossaClasse {
    private NossasDependencia nossaDependencia;

    public NossaClasse (NossasDependencia nossaDependencia){
       this.nossaDependencia = nossaDependencia;
    }

    public String retornaAlgumAtributo(){
       nossaDependencia.retornaAlgumAtributo();
    }
}

E, em nossa Activity:

public class NossaActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        NossaDependencia nossaDependencia = new NossaDependencia();
        NossaClasse nossaClasse = new NossaClasse(nossaDependencia);
    }
}

Para testar fica super fácil. Vamos usar o JUnit e o Mockito para isso. Caso não esteja familirializado com o Mockito, sugiro dar uma lida nesse post incrível do nosso engenheiro Tiago Lima sobre o assunto

public class TestandoNossaClasse {
    @Test
    public void deveRetornarAlgumAtributo(){
        NossaDependencia target = Mockito.mock(NossaDependencia.class);
        Mockito.when(nossaDependencia.retornaAlgumAtributo()).thenReturn("Dagger 2!");
        NossaClasse alvo = new NossaClasse(target);
        Assert.assertEquals(alvo.retornaAlgumAtributo(), "Dagger 2!");
    }
}

Conseguiu perceber? Podemos criar testes excepcionais apenas aplicando esse princípio de IoC! E, de bandeja, entendemos o que é "injetar uma dependência", já que a dependência está sendo fornecida para a NossaClasse.

Facilitando as coisas

Agora, vamos precisar utilizar a NossaClasse em vários lugares, e vai ser bem "chato" ficar repetindo essa linha:

NossaDependencia nossaDependencia = new NossaDependencia();
NossaClasse nossaClasse = new NossaClasse(nossaDependencia);

Poderiamos facilitar as coisas criando um pattern de Factory:

public class DependenciaFactory {
   private static NossaDependencia nossaDependencia;
   private static NossaClasse nossaClasse;

   public static NossaDependencia fornecerNossaDependencia(){
      if(nossaDependencia == null){
         nossaDependencia = new NossaDependencia();
      }
      return nossaDependencia;
   }

   public static NossaClasse fornececerNossaClasse(){
      if(nossaClasse == null){
         nossaClasse = new NossaClasse(fornecerNossaDependencia())
      }
      return nossaClasse;
   }
}

Agora, irá ficar mais transparente pra nós:

public class OutraActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        NossaClasse nossaClasse = DependenciaFactory.fornececerNossaClasse();
    }
}

Elegante, não é? Também achei.

Dagger 2

Agora que entendemos o por que de inverter as responsabilidades, podemos falar sobre o Dagger 2.

O que seria o Dagger 2? Seria, basicamente, o nosso DependenciaFactory, só que com uma implementação um pouco mais "avançada"!.

Cenário

Para explicar a arquitetura utilizada no Dagger 2, vamos fazer um paralelo com um cenário mais convencional para nós.

Vamos imaginar que iremos ter uma fábrica de cadeiras. Já temos as madeiras e os pregos, só precisamos das nossas ferramentas.

Bom, como iremos construir várias cadeiras, seria bom se comprassemos nossas ferramentas para não ter que ter o trabalho de ir buscá-las em algum lugar todas as vezes que iremos utilizá-las.

Também, seria ótimo se tivéssemos uma caixa de ferramentas para guardar todas elas. Caso comprássemos uma nova, era só jogar lá dentro.

Com o Dagger 2 nossas ferramentas são os Modules e nossa caixa de ferramentas são nossos Componentes! Ou seja, um Component nada mais é que um conjunto de Modules.

Criando nossas ferramentas (Modules)

Para se constuir uma cadeira, precisamos de pelo menos um martelo, certo? Temos vários martelos e precisamos fornecer esses martelos de alguma maneira:

@Ferramenta
public class Martelos {

    @Fornecedor
    public MarteloPequeno fornecerMarteloPequeno() {
        return new MarteloPequeno();
    }

    @Fornecedor
    public MarteloMedio fornecerMarteloMedio() {
        return new MarteloMedio();
    }

    @Fornecedor
    public MarteloGrande fornecerMarteloGrande() {
        return new MarteloGrande();
    }
}

Percebe que estamos usando @AlgumaCoisa? Isso é uma API chamada Annotation Processor! Você "anota" um método, e algum processador irá gerar todo o código para você. Sugiro que você dê uma lida no artigo que nosso engenheiro Felipe Theodoro escreveu sobre esse assunto!

Aqui, estamos anotando que nossa classe Martelos é uma fornecedora de ferramentas do tipo martelo, e que temos métodos que "fornecem" nossas classes.

O Dagger 2 tem dois processors que irão gerar toda as implementações necessárias. Vamos adaptar nossa classe para o "universo" do Dagger 2:

@Module
public class Martelos {

    @Provides
    public MarteloPequeno providesMarteloPequeno() {
        return new MarteloPequeno();
    }

    @Provides
    public MarteloMedio providesMarteloMedio() {
        return new MarteloMedio();
    }

    @Provides
    public MarteloGrande providesMarteloGrande() {
        return new MarteloGrande();
    }
}

Fácil, né? Agora, podemos "comprar" vários martelos e irá ser bem simples de fornecer (provides) eles para quem quiser usa-los.

Colocando as ferramentas na caixa

Agora, precisamos colocar isso tudo junto. Então, vamos colocar nossas ferramentas na caixa de ferramentas:

@CaixaDeFerramentas(ferramentas = {Martelos.class})
public interface NossaCaixaDeFerramentas {...}

Traduzindo pro dagger:

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

Pronto, "já está tudo lá".

Fornecendo as ferramentas

Temos todas as nossas ferramentas criadas. Agora, precisamos disponibilizá-las para todo mundo que quiser utilizar. Para isso, podemos colocar nossa caixa de ferramentas em um lugar que todo mundo tenha acesso: na Application:

public class NossaApplication extends Application {
    private static NossoComponent component;

    @Override
    public void onCreate() {
        super.onCreate();

        component = NossoComponent.builder()
                .martelos(new Martelos())
                .build();
    }
}

Mas pera lá. O NossoComponent tem um Builder Pattern? E esse método "martelos()"? Não temos nada.

Veja que o Dagger 2 gerou eles para nós, "automaticamente" (precisa fazer o build no projeto), e colocou um "Dagger" na frente, que é a classe gerada através do Annotation Processor que já vimos.

Vamos ajustar noso código:

component = DaggerNossoComponent.builder()
    .martelos(new Martelos())
    .build();

Ótimo.

Só precisamos "pegar" essa caixa de ferramenta de algum jeito, então:

public static NossoComponent getComponent() {
    return nossoComponent;
}

Definindo quem pode utilizar

Só temos um problema. Somos um pouco "ciumentos" com nossas ferramentas, assim como o Dagger 2. Ou seja, precisamos deixar explícito quem pode utilizá-las.

Na nossa caixa de ferramentas, vamos colocar algumas "etiquetas" com o nome de quem pode utilizá-las (classes):

@Component(modules = {Martelos.class})
public interface NossoComponent {
    void nossaActivityPodeUsarNossasFerramentas(NossaActivity nossaActivity);
}

Agora, precisamos pegar as ferramentas na nossa classe:

public class NossaActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      ...
      NossaApplication.getNossoComponent().nossaActivityPodeUsarNossasFerramentas(this);
  }
}

E como vamos utilizar nosso martelo? Simples:

@NossoMarteloMedio
MarteloMedio marteloMedio;

Agora, traduzindo tudo pro Dagger 2:

@Component(modules = {Martelos.class})
public interface NossoComponent {
    void inject(NossaActivity nossaActivity);
}

E usando o Martelo na nossa Activity:

public class NossaActivity extends AppCompatActivity {
    @Inject
    MarteloMedio marteloMedio;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        NossaApplication.getNossoComponent().inject(this);
    }
}

Momento Eureka!

Boas práticas

Em nossa caixa de ferramentas, podemos colocar várias ferramentas. Então, é bom "categorizá-las" de alguma maneira. Uma boa prática é agrupar todos os "martelos", "parafusos", "madeiras", "chaves de fenda" etc. Traduzindo para o mundo Java/Android:

component = DaggerNossoComponent.builder()
    .presenters(new PresenterModule())
    .repositories(new RepositoryModule())
    .clients(new ClientModule())
    .build();

Conclusão

Como vimos, podemos não utilizar o Dagger 2 em nosso projeto e termos o mesmo resultado. Porém, a praticidade e o poder que ganhamos com o Dagger 2, são muito vantajosos que acaba valendo muito a pena utilizamos.

No próximo post, irei falar sobre o uso avançado do Dagger 2 no nossos projetos, focando bastante em testes.

Gostou? Deixe suas impressões nesse post e também compartilhe!