Princípios básicos do Dagger

A injeção de dependência manual ou os localizadores de serviço em um app Android podem ser problemáticos, dependendo do tamanho do projeto. Você pode limitar a complexidade do seu projeto conforme ele se expande usando o Dagger para gerenciar dependências.

O Dagger gera automaticamente um código que imita o que você teria programado manualmente. Como o código é gerado no momento da compilação, ele é rastreável e tem melhor desempenho que outras soluções baseadas em reflexões, como o Guice.

As vantagens de usar o Dagger

O Dagger libera você do processo tedioso de ter que escrever códigos de texto clichê propenso a erros:

  • Gerar código AppContainer (gráfico do aplicativo) que você implementou manualmente na seção da ID manual.

  • Criar fábricas para as classes disponíveis no gráfico do aplicativo. É assim que as dependências são satisfeitas internamente.

  • Escolher reutilizar uma dependência ou criar uma nova instância usando escopos.

  • Criar contêineres para fluxos específicos, como você fez com o fluxo de login na seção anterior usando subcomponentes do Dagger. Isso melhora o desempenho do app ao liberar objetos na memória quando eles não são mais necessários.

O Dagger faz tudo isso automaticamente no tempo de compilação, contanto que você declare as dependências de uma classe e especifique como elas vão ser satisfeitas usando anotações. O Dagger gera um código semelhante ao que você escreveria manualmente. Internamente, o Dagger cria um gráfico de objetos que ele pode referenciar para encontrar a maneira de fornecer uma instância de uma classe. Para cada classe no gráfico, o Dagger gera uma classe do tipo fábrica class que a ferramenta usa internamente para receber instâncias desse tipo.

No momento da compilação, o Dagger revisa seu código e:

  • cria e valida gráficos de dependência, garantindo que:

    • as dependências de cada objeto possam ser atendidas, de modo que não haja exceções de tempo de execução.
    • não haja ciclos de dependência, portanto, não haja loops infinitos.
  • Gera as classes usadas no tempo de execução para criar os objetos reais e suas respectivas dependências.

Um caso de uso simples no Dagger: como gerar uma fábrica

Para demonstrar como você pode trabalhar usando o Dagger, vamos criar uma fábrica simples para a classe UserRepository mostrada no diagrama a seguir:

Defina o UserRepository da seguinte maneira:

Kotlin

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }

    ...
}

Adicione uma anotação @Inject ao construtor UserRepository para que o Dagger saiba como criar um UserRepository:

Kotlin

// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    // @Inject lets Dagger know how to create instances of this object
    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

No snippet de código acima, você está informando ao Dagger:

  1. como criar uma instância UserRepository com o construtor anotado @Inject.

  2. quais são as dependências, UserLocalDataSource e UserRemoteDataSource.

Agora, o Dagger sabe criar uma instância de UserRepository, mas não sabe como criar as dependências. Se você anotar as outras classes também, o Dagger saberá como criá-las:

Kotlin

// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

Java

public class UserLocalDataSource {
    @Inject
    public UserLocalDataSource() { }
}

public class UserRemoteDataSource {
    @Inject
    public UserRemoteDataSource() { }
}

Componentes do Dagger

O Dagger pode criar um gráfico das dependências no seu projeto que pode ser usado para descobrir onde ele precisará recebê-las quando for necessário. Para que o Dagger faça isso, é necessário criar uma interface e anotá-la com @Component. O Dagger cria um contêiner da mesma forma que você faria com a injeção manual de dependência.

Dentro da interface @Component, é possível definir funções que retornam instâncias das classes necessárias (ou seja, UserRepository). O @Component instrui o Dagger a gerar um contêiner com todas as dependências necessárias para satisfazer os tipos que ele expõe. Isso é chamado de componente Dagger e contém um gráfico que consiste nos objetos que o Dagger sabe como fornecer e nas suas respectivas dependências.

Kotlin

// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be provided from the container
    fun repository(): UserRepository
}

Java

// @Component makes Dagger create a graph of dependencies
@Component
public interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be consumed from the graph
    UserRepository userRepository();
}

Quando você cria o projeto, o Dagger gera uma implementação da interface ApplicationGraph para você: DaggerApplicationGraph. Com o processador de anotações, o Dagger cria um gráfico de dependência que consiste nas relações entre as três classes (UserRepository, UserLocalDatasource e UserRemoteDataSource) com apenas um ponto de entrada: receber uma instância UserRepository. Você pode usá-lo da seguinte maneira:

Kotlin

// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()

Java

// Create an instance of the application graph
ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

// Grab an instance of UserRepository from the application graph
UserRepository userRepository = applicationGraph.userRepository();

O Dagger cria uma nova instância de UserRepository sempre que solicitado.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository != userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository != userRepository2)

Às vezes, você precisa ter uma instância exclusiva de uma dependência em um contêiner. Você pode querer isso por vários motivos:

  1. Quer que outros tipos que têm esse tipo como dependência compartilhem a mesma instância, como vários objetos ViewModel no fluxo de login usando o mesmo LoginUserData.

  2. Um objeto é algo que leva tempo para ser criado, e você não quer ter que criar uma nova instância sempre que for declarada como uma dependência (por exemplo, um analisador JSON).

No exemplo, convém ter uma instância exclusiva de UserRepository disponível no gráfico para que sempre que solicitar um UserRepository, você receba a mesma instância. Isso é útil no seu exemplo porque, em um aplicativo real com um gráfico de aplicativo mais complexo, você pode ter vários objetos ViewModel dependendo de UserRepository e não quer ter que criar novas instâncias de UserLocalDataSource e UserRemoteDataSource sempre que UserRepository precisar ser fornecido.

Na injeção manual de dependência, você faz isso passando a mesma instância de UserRepository para os construtores das classes ViewModel. Porém, no Dagger, como você não está escrevendo esse código manualmente, é necessário informar que você quer usar a mesma instância. Isso pode ser feito usando anotações de escopo.

Escopo com o Dagger

Você pode usar anotações de escopo para limitar o ciclo de vida de um objeto ao do componente. Isso significa que a mesma instância de uma dependência é usada sempre que esse tipo precisa ser fornecido.

Para ter uma instância exclusiva de um UserRepository ao solicitar o repositório em ApplicationGraph, use a mesma anotação de escopo para a interface @Component e para UserRepository. Você pode usar a anotação @Singleton que já vem com o pacote javax.inject que o Dagger usa:

Kotlin

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@Singleton
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

Como alternativa, é possível criar e usar uma anotação de escopo personalizado. Você pode criar uma anotação de escopo da seguinte forma:

Kotlin

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Java

// Creates MyCustomScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

Em seguida, você pode continuar como antes:

Kotlin

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val service: UserService
) { ... }

Java

@MyCustomScope
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

@MyCustomScope
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

Nos dois casos, o objeto é fornecido com o mesmo escopo usado para anotar a interface @Component. Assim, sempre que você chamar applicationGraph.repository(), receberá a mesma instância de UserRepository.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository == userRepository2)

Conclusão

É importante estar ciente dos benefícios do Dagger e das noções básicas de como ele funciona antes de usá-lo em cenários mais complicados.

Na próxima página, você vai aprender a adicionar o Dagger a um app Android.