Adotando Dagger2: Todos os passos necessários

Dagger2 é a framework preferida de Inversão de Dependências da comunidade Android. Mas começar a usar a Dagger2 nem sempre é algo fácil. O problema é que para usar Dagger2 é necessário ter bons conhecimentos sobre arquitetura de software, e muitos tutoriais pulam essa parte e tentam ensinar Dagger2 sem o conhecimento básico.

Este artigo tem como objetivo mostrar todos os conceitos necessários para adoção da Dagger2 em um projeto Android.

Para exemplificar a teoria iremos refatorar um aplicativo que foi inicialmente desenvolvido sem uma arquitetura; todo bagunçado. A refatoração está dividida em cinco partes, e cada uma introduz um novo conceito.

Mas antes vamos ver um pouco de teoria para deixar claro onde queremos chegar com a Dagger2.

Injeção de Dependências ou Princípio da Inversão de Dependências?

Dagger2 é uma framework para injeção de dependências, ou Dependency Injection (DI). Para uma arquitetura limpa, o DI é geralmente usado em conjunto com o Princípio da Inversão de Dependências, ou Dependency Inversion Principle (DIP). DI e DIP são dois padrões distintos.

Vamos mostrar a diferença entre DI e DIP com um exemplo:

class Jogador {
int jogarDado() {
Random r = new Random();
return r.nextInt(42) + 1;
}
// outras funções
}

Essa classe Jogador depende da classe Random. Além disso, ela está controlando como a instância da Random é criada. Podemos usar o DI para "injetar" essa dependência:

class JogadorDI {
private Random r;

JogadorDI(Random r) {
this.r = r;
}

int jogarDado() {
return r.nextInt(42) + 1;
}
// outras funções
}

A classe JogadorDI não controla mais como a classe Random é criada. E isso é a injeção de dependências.

Porém, depender de uma classe “baixo nível” como a Random geralmente é algo problemático. O ideal seria que a classe Jogador usasse uma classe no mesmo nível de abstração. E isso seria o DIP:

interface Dado {
int jogar();
}

class JogadorDIP {
private Dado dado;

JogadorDIP(Dado dado) {
this.dado = dado;
}

int jogarDado() {
return dado.jogar();
}
// outras funções
}

A classe JogadorDIP está usando o DIP para abstrair a operação de jogar um dado; que agora foi delegado para outra classe que implemente a interface Dado.

Note também que a classe JogadorDIP não precisa saber como a classe Dado é implementada, assim deixando o código mais fácil de manter. Essa seria a ideia do Princípio de Responsabilidade Única, ou Single Responsibility Principle (SRP).

DIP? Pra que?

Existem diversas discussões na internet sobre os benefícios do DIP. Vou deixar aqui apenas exemplos concretos e que formam o foco desse artigo:

Testes

Pergunta: Teria como testar a classe Jogador? Testar com os valores aleatórios gerados pela Random seria muito difícil. O certo seria algo previsível. Então não, não daria de testar essa classe.

Com a classe JogadorDI daria de iniciar uma instância da Random com uma seed estática ou como um mock. Isso permitiria testes previsíveis, né? Talvez, mas, a classe JogadorDI precisa mesmo gerar números “aleatórios” para floats, doubles, booleans, etc? Não. Então, estamos só perdendo tempo passando algo baixo nível para a JogadorDI, pois testar todos os casos fica mais complicado.

JogadorDIP é a versão mais fácil de ser testada. Podemos mockar a interface Dado, assim retornando exatamente o que a gente quiser durante os testes!

Abstração de detalhes

A classe Jogador está apenas interessada em jogar dados. A geração de números aleatórios é um detalhe que a classe Jogador não precisa saber.

Quando a gente passa uma abstração, como a interface Dado, para a classe JogadorDIP, a gente está simplificando o nosso código. Ao abstrair os detalhes, a classe JogadorDIP não precisa mais se preocupar como o dado é lançado, podendo assim se concentrar em resolver apenas o problema dela.

Mudança de tecnologia

Dado é uma interface, logo, podemos ter várias implementações. Uma implementação pode usar a Random, mas podemos ter uma outra que faz um request HTTP, por exemplo.

Agora, imagine que ao invés de um dado a gente esteja abstraindo um repositório. Inicialmente podemos fazer um repositório “in memory” só para testar o app. Quando necessário a gente pode fazer uma implementação usando SQLite. E, daqui 1 ano, uma mudança para Realm seria trivial.

Leituras complementares:

O App

Para ilustrar os passos, vamos refatorar o app Catter2. Esse app tem como objetivo salvar imagens de gatinhos na conta do usuário.

O app tem três telas:

  • LoginActivity: Faz a autenticação do usuário;
  • FavoritesActivity: Listagem das imagens favoritas do usuário;
  • ListActivity: Lista imagens de gatinhos. Usuário pode clicar para adicionar imagens na sua lista de favoritos.

O código inicial se encontra [nesse link].

Terceiros

  • A persistência dos favoritos é feita usando SharedPreferences;
  • As imagens dos gatinhos são requisitadas do serviço TheCatApi.com usando Retrofit;
  • TheCatApi retorna apenas URLs. O download das imagens é feito com a Picasso.

Passo 0: God Activities e o Princípio da Responsabilidade Única

God object is an object that knows too much or does too much
–Wikipedia (https://en.wikipedia.org/wiki/God_object)

Sabemos que queremos usar DIP em nosso app, mas não temos como utilizá-lo ainda pois não há nada para ser injetado.

Vamos analisar a FavoritesActivity: [FavoritesActivity]. Ela está:

  • Carregando a lista de favoritos do disco
  • Processando as imagens favoritas
  • Requisitando as imagens da internet
  • Mostrando as imagens na tela
  • Lidando com a navegação entre as telas
  • Implementando lógica de reação quando botões são clicados

Esse é um exemplo de uma God Activity, ou seja, uma Activity que está fazendo muita coisa.

O primeiro passo para adotar DIP seria a identificação dos diversos componentes do sistema, levando em consideração o Princípio da Responsabilidade Única, ou Single Responsibility Principle (SRP).

Uma classe deve ter apenas uma razão para mudança
–Traduzido de UncleBob (http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)

Adotando o Princípio da Responsabilidade Única (SRP)

O SRP é essencial para a adoção do DIP, por isso vamos adotá-lo logo no primeiro passo! Para isso vamos dividir as nossas God Activities em objetos menores, onde cada um tenha apenas uma responsabilidade.

Primeiramente precisamos identificar as diversas responsabilidades do app:

  • Verificação das credenciais do usuário
  • Carregamento das imagens favoritas do usuário
  • Adicionamento de imagens na lista de favoritos do usuário
  • Persistência da lista de favoritos do usuário
  • Baixar da internet lista de fotos de gatinhos
  • Mostrar fotos de gatinhos pro usuário
  • Navegação entre as telas
  • Lógica de reação quando botões são clicados

Com as responsabilidades identificadas, podemos dividir elas em classes:

  • LoginService: Verifica as credenciais do usuário; retorna um token quando a autenticação é realizada com sucesso;
  • FavoritesRepository†: Carrega e salva a lista de favoritos do usuário;
  • GetFavoritesUseCase: Transforma a lista de favoritos do usuário em uma lista de URLs de fotos de gatinhos;
  • AddFavoriteUseCase: Adiciona uma imagem a lista de favoritos do usuário;
  • FetchCatImagesUseCase: Baixa lista de imagens do site TheCatApi.com;
  • LoginUseCase: Usa o LoginService para autenticar o usuário;

† Segue o Padrão de Repositório: “Repository” de Edward Hieatt e Rob Mee.

Uma vez que temos os nomes das classes definidos e a função de cada uma, podemos então implementar o primeiro passo. Veja o código: [Passo 0].

Pontos importantes do código: Tudo relacionado aos favoritos foram movidos para o pacote favorites. UseCase‘s usam Repository‘s e Service‘s. As Activity‘s usam UseCase‘s.

Esse é o passo mais extenso, porém o mais importante deste artigo! O código melhorou de forma considerável. Os nomes das classes deixam claro quais as responsabilidades de cada uma. Códigos duplicados foram removidos com o FavoritesRepository. E, o mais importante, as nossas Activity ‘s estão bem mais fáceis de entender 😸

Nota: As Activity‘s continuam tendo muitas responsabilidades, como navegação e lógica de como reagir a cliques de botões. Vamos deixar assim por simplicidade.

Arquitetura em Camadas

A seção anterior introduziu quatro tipos de classes: Service‘s, Repository‘s, UseCase‘s e Activity‘s. Essas classes vão ser divididas em três camadas: Infrastructure, Domain e Presentation.

A imagem acima mostra como é feita a interação entre as camadas. Dois pontos são importantes:

  • Dependências seguem apenas uma direção: a Domain sabe nada sobre a Presentation.
  • Uma camada depende apenas da camada diretamente abaixo: a Presentation sabe nada sobre a Infrastructure.

Na prática, a primeira regra diz que nossos Service‘s e Repository‘s são totalmente independentes das Activities e UseCase‘s. Assim como os UseCase‘s não sabem sobre as Activities.

A segunda regra diz que a Presentation (Activities) não sabe sobre Service‘s e Repository‘s. Para acessar um serviço ou um repositório, a Activity precisa usar um UseCase da Domain. (Verifique se você realmente entendeu essas duas regras, elas são super importantes)

Essas regras existem para simplificar as dependências do nosso app, assim facilitando a manutenção e os testes do nosso app, que veremos nas próximas seções!

Leituras complementares:

Passo 0 resumo

Identifique as responsabilidades de cada classe. Separe-as para que cada uma tenha apenas uma responsabilidade. Use uma arquitetura que simplifique as suas dependências e que seja fácil de testar.

Passo 1: Testando um UseCase

Um app real vai ter a maior parte da lógica em UseCase ‘s, que fazem parte da camada de negócios, ou Domain. Por isso é importante construir UseCase ‘s que sejam fáceis de testar. Porém, veja o estado atual do AddFavoriteUseCase: [AddFavoriteUseCase].

Nesse exemplo, o repositório está sendo criado pelo UseCase. Esse é o mesmo problema descrito na seção sobre DI ou DIP. Esse UseCase é difícil de ser testado pois ele está usando a implementação do repositório que usa SharedPreferences.

A solução é simples. Ao invés de deixar o UseCase criar a instância do repositório, passa o repositório por parâmetro, como vimos nas primeiras seções deste artigo. Assim, é possível fazer um mock desse repositório durante os testes.

O resultado do UseCase usando o DIP é esse: [AddFavoriteUseCase]. Bem simples não? ;)

Uma interface, várias implementações

Na versão do nosso código do passo 0, a classe FavoritesRepository é uma classe concreta; ela pode ser instanciada. Porém, nem sempre queremos usar a mesma implementação. Por exemplo, durante os testes a gente pode querer usar um stub no lugar. Ou quando testar algumas funcionalidades do app, as vezes é mais fácil usar um repositório “in memory”, que não faz persistência dos dados.

Aqui está a nossa nova interface: [FavoritesRepository]. E a implementação: [SharedPrefFavoritesRepository].

Leituras Complementares

Passo 1 resumo

Use DIP nos seus UseCase‘s. Use interface para seus serviços e repositórios. Isso facilita mock e stubs durante testes.

Passo 2: Tempo de vida das instâncias

Esse terceiro passo é super importante! Para ilustrá-lo, vamos primeiro descrever um problema: vamos adicionar um cache pros requests para a TheCatApi.

Ps: O passo anterior introduziu a interface TheCatAPI: [TheCatApi(Passo1)].

Adicionar esse cache é simples, né? Veja: [CacheTheCatAPI]. Note como todo o resto do sistema permanece sem mudanças. O FetchCatImagesUseCase vai agora usar essa versão com cache, e não precisou de nenhuma alteração! 😻

Porém, ao rodar o app o cache parece não estar fazendo nada. O problema é que a instância do CacheTheCatApi está sendo criada durante a criação da ListActivity. E por causa disso, a cache é recriada toda vez que há uma mudança de configuração (rotação do dispositivo, etc).

Para solucionar basta aumentar o tempo de vida da cache fazendo com que ela sobreviva o ciclo de vida da ListActivity.

Tempo de vida das instância

O tempo de vida de uma instância determina quando ela é criada e destruída. O diagrama de sequência acima mostra como será organizado o tempo de vida das nossas instâncias.

A instância da TheCatAPI será criada no início do app e nunca será destruída (não tem um “X”-zinho no final da lifeline). Já os UseCase‘s vão sobreviver só enquanto a Activity usando eles sobreviver.

O tempo de vida do FavoritesRepository está associado ao usuário. Ele é instanciado quando o usuário se loga, e destruído quando o usuário desloga.

Acessando a TheCatAPI na ListActivity

Sabemos que a instância da TheCatAPI sobrevive o ciclo de vida de uma Activity, mas como acessar essa instância de dentro da Activity não é tão fácil.

O problema é que uma Activity não tem um construtor "normal" do Java, onde a gente poderia passar a TheCatAPI como parâmetro (igual fizemos com o UseCase). Também não é possível pegar a instância da Activity quando criamos ela com context.startActivity(intent).

Uma solução é armazenar a instância da TheCatAPI em uma variável "static" com acesso global. E, como seu tempo de vida é o mesmo da aplicação, podemos colocá-la na classe Application: [TheCatAPI tempo de vida].

Com isso a ListActivity pode facilmente acessar a instância singleton da TheCatAPI, e o cache agora funciona perfeitamente!

Tempo de vida do FavoritesRepository

A instância do FavoritesRepository foi organizada usando a mesma estratégia da seção anterior. Entretanto, essa instância só existe enquanto o usuário está logado. Veja o diff: [Tempo de vida do Usuário].

Note que estamos usando a classe App para armazenar a instância do repositório, mesmo que seu tempo de vida não seja o mesmo da aplicação. Iremos arrumar isso em breve.

Também importante notar que as Activity's não precisam mais receber o userToken como parâmetro, pois elas não têm mais a responsabilidade de instanciar o FavoritesRepository. Isso é ótimo pois as Activity's agora têm uma responsabilidade a menos, yay 😹

O diff completo para esse passo está aqui: [Passo2].

Passo2 resumo

Planeje o tempo de vida das suas instâncias; determine quando elas serão criadas e destruídas. Activity's podem acessar essas instâncias através de variáveis estáticas e globais.

Passo 3: DI Components

O Princípio da Responsabilidade Única foi violado quando movemos a inicialização e distribuição das classes TheCatAPI e FavoritesRepository para a classe App, pois agora essa classe tem várias responsabilidades. Vamos resolver esse problema criando dois tipos de classes: Modules e Components.

Além disso, a instância da FavoritesRepository só existe quando o usuário está logado, então vamos tirar ela da classe App e colocar em uma classe mais apropriada.

DI Modules

Modules, ou módulos, ficam responsáveis pela criação das instâncias. Cada módulo é composto por um conjunto de métodos "provide". Veja o [TheCatAPIDIModule] and e sua implementação padrão: [CachedRetrofitCatAPIDIModule].

Dica: Criar uma interface para o módulo não é obrigatório, mas pode ser útil para quando se tem múltiplas formas de inicializar uma instância. Por exemplo, você pode ter um módulo que cria um repositório usando Firebase, enquanto um outro módulo cria o repositório "in-memory".

DI Components

Os Components servem para armazenar e distribuir as instâncias criadas pelos Modules. Cada Component tem o seu próprio tempo de vida, e todas as suas instâncias tem o mesmo tempo de vida do Component.

Vamos analisar o [AppDIComponent]; o Component para o tempo de vida da aplicação. Ele tem uma instância estática, como era feito na classe App. Ele tem o método initialize para a sua inicialização com todos os Modules necessários. A instância da TheCatAPI é distribuída através de um método público getTheCatAPI. O AppDIComponent é inicializado junto com o app: [App].

O UserDIComponent é o Component com o tempo de vida para quando o usuário está logado no sistema: [UserDIComponent]. Ele funciona da mesma forma que o AppDIComponent , porém depende do AppDIComponent através do [FavoritesRepoDIModule]. Além disso, o UserDIComponent repassa as instâncias do AppDIComponent para o resto do sistema.

O UserDIComponent é inicializado quando o usuário loga no sistema: [LoginActivity(Passo3)].

Nota: Components podem depender de outros components. Modules não!

As Activities agora podem requisitar as instâncias necessárias pro UserDIComponent, veja [Activity(Passo3)].

O diff do passo completo: [Passo2->Passo3].

Passo3 resumo

Modules criam as instâncias. Components armazenam e distribuem elas.

Um Component tem um tempo de vida. Por exemplo: AppDIComponent está associado ao tempo de vida da aplicação, já o UserDIComponent só existe enquanto o usuário estiver logado.

Passo 4: Testando uma Activity

Já é possível testar Activities com o nosso sistema atual. Veja essa tentativa de teste: [Primeira tentativa de teste de uma Activity].

Esse teste funciona, mas ele tem um grande problema: a Activity, que fica na camada de Presentation, está mockando objetos da camada de Infrastructure. Isso é terrível pois estamos também testando os UseCases da camada Domain.

Devemos sempre testar apenas uma camada. Logo, uma Activity (Presentation) deve receber como mocks apenas objetos da camada Domain.

Injetando UseCases nas Activities

Para que a gente possa mockar os UseCases durante os testes, precisamos achar uma forma de injetá-los na Activity. O problema é que uma Activity não tem constructors, então temos que injetar essas dependências através de métodos, veja: [FavoritesActivity, inject].

No diagrama de sequencia anterior, vimos que cada Activity tem seu próprio tempo de vida. Logo, criamos um Component e um Module para cada Activity. O FavoriteActivityDIComponent, veja [aqui], é um pouco diferente dos componentes vistos anteriormente. Como o Component tem o mesmo tempo de vida da Activity, armazenamos ele na própria Activity e não em variáveis estáticas. Além disso, usamos métodos para injetar dependências ao invés dos métodos get de antes.

O FavoriteActivityModule usa variáveis estáticas para permitir que a gente possa injetar UseCases mockados no app.

Agora o teste FavoritesActivityTest pode testar a Activity mockando apenas os UseCases, veja: [Code].

Todas as mudanças do Passo 4 estão neste link: [Passo3->Passo4].

Passo4 Resumo

Usamos DI com nossas Activities também, porém injetamos as dependências através de métodos e não de constructors.

Passo 5: Dagger2!

O nosso sistema de DI e DIP já está pronto! Até aqui cobrimos os principais desafios que encontramos ao introduzir esses conceitos em um projeto. Porém, podemos simplificar a nossa implementação usando uma framework de DI como a Dagger2.

Uma vez que você entende como DI e DIP funcionam, adicionar Dagger2 no projeto acaba virando uma tarefa trivial, como veremos a seguir.

Tempo de vida da Aplicação

Vamos começar a migração pra Dagger2 com o AppDIComponent. Na Dagger2, um Component tem as mesmas responsabilidades dos Components discutidos até então; eles armazenam as instâncias providenciadas pelos módulos e fazem a distribuição deles para o resto do sistema.

A implementação desse componente com a Dagger2 está [Aqui]. A anotação @Singleton é usada para indicar que este componente é um Singleton, ou seja, seu tempo de vida é o mesmo da aplicação.

A anotação Component é usada para indicar um componente da Dagger2. Essa anotação deve ser feita em uma interface ou classe abstrata. Você passa uma lista de módulos como parâmetros para ela.

Assim como feito anteriormente, vamos armazenar esse componente em uma variável estática e oferecer um método initialize. A inicialização é um pouco diferente pois agora precisamos usar códigos gerados pela Dagger2.

Dica: Dagger2 usa geração de código para criar a classe DaggerAppDIComponent. Se você está tendo erros, tente dar um build no seu projeto.

Veja como os métodos "get" ficaram super simples. Isso por que a Dagger2 está gerando todo o código para eles, basta a gente declarar o tipo de retorno do método e pronto! Só lembre-se de usar métodos abstratos.

O AppDIModule é um pouco diferente da versão anterior. Dagger2 exige classes concretas para os módulos. Para manter o mesmo estilo implementado aqui anteriormente, onde se tem uma interface pro módulo e várias implementação, vamos lançar exceptions para os métodos da classe base, veja [Aqui]. A implementação dos módulos continuam a mesma, veja [Aqui].

Não é necessário usar as anotações da Dagger2 nos módulos derivados das classes base.

A inicialização na classe App continua exatamente igual: [App].

Tempo de Vida do Usuário

Vamos estudar oUserDIComponent agora: [Código].

O componente usa a anotação UserScope ao invés da Singleton. Essa anotação é usada para indicar o escopo do componente. Leituras adicionas sobre escopos na Dagger2 estão no final desta seção.

Estamos agora passando a lista de dependências desse componente para a anotação @Component. Isso permite que os módulos do UserDIComponent possam acessas instâncias do AppDIComponent. Por isso precisamos passar a instância do AppDIComponent pro UserDIComponent no momento de sua criação.

Vamos focar no FavoritesRepoDIModule agora, veja [Código]. O método provideFavoritesRepository agora tem uma lista de parâmetros. Eles são usados para criar uma instância do FavoritesRepository. Dagger2 irá, automaticamente, encontrar todos esses parâmetros e irá passá-los para o método.

O userToken providenciado pelo método provideUserToken está disponível apenas para os módulos do UserDIComponent. O resto do sistema, incluindo outros componentes, não podem acessar essa instância pois o UserDIComponent não tem nenhum método "get" para ela.

A anotação @Named é usada para a instância do userToken pois ela é uma String, um tipo muito conhecido. Dagger2 só enxerga o tipo do retorno do método na hora de determinar o tipo da instância. Logo, caso haja dois métodos "provide" que retornam String, haverá um erro. Para diferenciar esses métodos, basta usar a anotação @Named.

Dica: Não use a anotação @Named pois é fácil escrever o nome errado. Crie novos tipos como uma classe chamada UserToken e use ela como retorno de seus métodos.

Tempo de Vida das Activities

Vamos ver a implementação da FavoritesActivity mudou com a Dagger2. O FavoritesActivityDIComponent ([Código]) mudou um pouco. Ele usa um escopo personalizado, como o UserScope. Além disso, não precisamos mais implementar o método inject, pois a Dagger2 faz isso. A instancia do componente não precisa ser armazenada, por isso criamos o componente e já injetamos as dependências na Activity.

Há apenas uma diferença significativa no FavoritesActivityDIModule ([Código]), que é o uso de parâmetros do tipo dagger.Lazy<T>. Ao usar o tipo Lazy<FavoritesRepository>, estamos dizendo pra Dagger2 não tentar instanciar um FavoritesRepository até que seja realmente necessário. Sem o Lazy<T>, a Dagger2 irá instanciar o repositório antes de chamar o método provideGetFavoritesUseCase, mas note como não usamos esse repositório quando estamos testando o app (a variável testGetFavoritesUseCase não será nula), logo nem sempre precisamos do repositório.

Na nossa Activity não usamos mais o método injectUseCase. Ao invés disso, anotamos as propriedades que queremos injetar com a anotação @Inject. Veja: [Código].

Todos as modificações do passo 5 estão nesse [Diff].

Passo5, the end

E é isso! O nosso app está agora usando DI, DIP e Dagger2, com um pouco de Clean Architecture! Na minha opinião, esse último passo é o mais fácil de todos. Uma vez que você tem uma boa arquitetura usando DIP, e SOLID, adicionar Dagger2 acaba sendo uma tarefa fácil 🗡️🍰️

Leituras Complementares

Podemos melhorar o código ainda mais?

SIM! O app já melhorou consideravelmente desde sua versão original, porém está longe de ser usado como referência. Quem tiver afim de praticar os conceitos abordados neste post, ou quiser melhorar algo, basta forkiar o projeto e abrir um issue pra comunidade ver: https://github.com/AllanHasegawa/Catter2

Vou deixar uma lista não exaustiva de coisas que podem ser melhoradas:

LoginActivity

Toda a funcionalidade de "login" não foi implementada corretamente usando DIP. Note que nem temos um LoginDIComponent. Refatorar a LoginActivity usando as técnicas abordadas neste post pode se um bom exercício.

Adicionar novas funcionalidades/implementações

Uma coisa legal da nossa arquitetura é que agora está bem fácil de adicionar novas funcionalidade e/ou mudar implementações. Por exemplo, seria bem trivial fazer a persistência dos favoritos usando StorIO. Ou talvez adicionar uma funcionalidade para remover images dos favoritos.

[Avançado] Usar uma framework MV*

Nossas Atividades ainda estão com muitas responsabilidades. Uma forma de resolver isso é usando uma framework MV*. Talvez seja necessário criar uma nova camada na arquitetura e repensar no tempo de vida das instâncias.

Usando Dagger2 de outras formas

Não existe "a" forma correta de usar Dagger2. Esse post mostrou apenas uma forma de usar a Dagger2. Caso alguém saiba outras formas de usar Dagger2, então, por favor, mostre pra gente :)

Conclusões

Dagger2 é uma framework bem simples, porém, para adotá-la é necessário ter conhecimentos de DI, DIP, arquitetura e de testes. Espero que este artigo tenha conseguido mostrar todas as partes necessárias para adoção da Dagger2.

Vou deixar um resumo do que discutimos:

  • Classes devem ter apenas uma responsabilidade
  • Evite colocar lógica de negócio nas suas Activities, Fragments, Etc.
  • Organize suas classes em camadas; use apenas dependências unidirecionais
  • Nunca use mais de duas camadas em uma classe
  • Tente sempre desenvolver classes testáveis; use DIP sempre que necessário

Quando você estiver projetando os seus módulos e componentes pra Dagger2, tente:

  • Identificar a responsabilidade das suas classes
  • Separe as classes se necessário
  • Determine o tempo de vida delas
  • Como você irá criar as suas instâncias? (Módulo)
  • Quem vai ter acesso a essas instâncias? (Componente)
  • Uma vez que você resolveu as perguntas anteriores, você implementa com a Dagger2

E é isso. Espero que este artigo tenha sido útil 🙏 Até mais o/