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:
- “Inversion of Control Containers and the Dependency Injection pattern” de Martin Fowler
- “DIP in the Wild” de Brett L. Schuchert
- “S.O.L.I.D no Android” de Iago Mendes Fucolo
- “Introdução ao Dagger2” de Caique Oliveira
- “The Principles of OOD” de UncleBob
- “Repository” de Edward Hieatt e Rob Mee
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 umtoken
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 oLoginService
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:
- Android Clean-Architecture GitHub
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
- “Mocks Aren’t Stubs” de Martin Fowler
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
- “Dependency injection with Dagger 2 — Custom scopes” de Miroslaw Stanek
- "Dagger 2 scopes demystified"
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/