Injeção de dependência no Android

A injeção de dependência (DI, na sigla em inglês) é uma técnica amplamente usada na programação e adequada para o desenvolvimento do Android. Ao seguir os princípios de DI, você cria a base para uma boa arquitetura do app.

A implementação da injeção de dependência oferece as seguintes vantagens:

  • Reutilização do código
  • Facilidade de refatoração
  • Facilidade de teste

Conceitos básicos da injeção de dependência

Antes de abordar especificamente a injeção de dependência no Android, esta página fornece uma visão mais geral de como a injeção de dependência funciona.

O que é a injeção de dependência?

As classes geralmente exigem referências a outras classes. Por exemplo, uma classe Car pode precisar de uma referência a uma classe Engine. Essas classes solicitadas são chamadas de dependências e, neste exemplo, a classe Car depende de ter uma instância da classe Engine para ser executada.

Há três maneiras de uma classe conseguir um objeto de que precisa:

  1. A classe cria a dependência necessária. No exemplo acima, Car criaria e inicializaria a própria instância de Engine.
  2. Pegue-o de outro lugar. Algumas APIs do Android, como getters Context e getSystemService(), funcionam dessa maneira.
  3. Forneça-o como um parâmetro. O app pode disponibilizar essas dependências quando a classe é criada ou transmiti-las nas funções que precisam de cada dependência. No exemplo acima, o construtor Car receberia Engine como um parâmetro.

A terceira opção é a injeção de dependência. Com essa abordagem, você usa as dependências de uma classe e as disponibiliza, em vez de fazer com que a instância da classe as receba.

Veja um exemplo. Sem a injeção de dependência, a representação de um Car que cria sua própria dependência Engine no código é semelhante ao seguinte:

Kotlin

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
Classe de carro sem injeção de dependência

Este não é um exemplo de injeção de dependência porque a classe Car está criando o próprio Engine. Isso pode ser problemático porque:

  • Car e Engine estão fortemente acoplados. Uma instância de Car usa um tipo de Engine, e nenhuma subclasse ou implementação alternativa pode ser facilmente usada. Caso o Car construa o próprio Engine, você precisaria criar dois tipos de Car em vez de apenas reutilizar o mesmo Car para mecanismos de tipo Gas e Electric.

  • A forte dependência de Engine dificulta os testes. Car usa uma instância real de Engine. Isso evita o uso de um teste duplo para modificar Engine para diferentes casos

Qual é a aparência do código com a injeção de dependência? Em vez de cada instância de Car criar o próprio objeto Engine na inicialização, ele recebe um objeto Engine como um parâmetro no próprio construtor:

Kotlin

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

Java

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}
Classe de carro usando injeção de dependência

A função main usa Car. Como Car depende den Engine, o app cria uma instância de Engine e a usa para criar uma instância de Car. Os benefícios dessa abordagem baseada na injeção de dependência são:

  • Reutilização de Car. É possível transmitir implementações diferentes de Engine para Car. Por exemplo, é possível definir uma nova subclasse de Engine chamada ElectricEngine que você quer que Car use. Se usar a injeção de dependência, tudo o que você precisará fazer é transmitir uma instância da subclasse ElectricEngine atualizada, e Car ainda funcionará sem precisar de mudanças.

  • Teste fácil de Car. É possível transmitir cópias de teste para testar diferentes cenários. Por exemplo, você pode criar um teste duplo de Engine chamado FakeEngine e configurá-lo para testes diferentes.

Há duas maneiras principais de fazer a injeção de dependência no Android:

  • Injeção de construtor. Esta é a maneira descrita acima. Você transmite as dependências de uma classe para o construtor.

  • Injeção de campo (ou injeção de setter) Algumas classes de framework do Android, como atividades e fragmentos, são instanciadas pelo sistema, e por isso, a injeção de construtor não é possível. Com a injeção de campo, as dependências são instanciadas após a criação da classe. O código ficaria assim:

Kotlin

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

Java

class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

Injeção de dependência automática

No exemplo anterior, você criou, forneceu e gerenciou as dependências das diferentes classes sem precisar de uma biblioteca. Isso é chamado de injeção manual de dependência ou injeção de dependência manual. No exemplo Car, existia apenas uma dependência, mas mais dependências e classes podem tornar a injeção manual de dependências mais tediosa. A injeção de dependência manual também apresenta vários problemas:

  • Para apps grandes, conseguir todas as dependências e conectá-las corretamente pode exigir uma grande quantidade de código de texto clichê. Em uma arquitetura com várias camadas, para criar um objeto para uma camada superior, é necessário disponibilizar todas as dependências das camadas abaixo dela. Como exemplo, para construir um carro real, você pode precisar de um motor, uma transmissão, um chassi e outras peças. O motor, por sua vez, precisa de canos e faíscas.

  • Quando não for possível criar dependências antes de transmiti-las, por exemplo, no caso de inicializações lentas ou objetos de escopo para fluxos do app, você precisará gravar e manter um contêiner personalizado (ou gráfico de dependências) que gerencie o ciclo de vida delas na memória.

Existem bibliotecas que resolvem esse problema automatizando o processo de criação e disponibilizando dependências. Elas se encaixam em duas categorias:

  • Soluções baseadas em reflexões que conectam dependências no tempo de execução.

  • Soluções estáticas que geram o código para conectar dependências no tempo de compilação.

Dagger é uma biblioteca popular de injeção de dependência para Java, Kotlin e Android mantida pelo Google. A Dagger facilita o uso de DI no seu app, criando e gerenciando o gráfico de dependências para você. Ela fornece dependências totalmente estáticas e no tempo de compilação, resolvendo muitos dos problemas de desenvolvimento e desempenho com soluções baseadas em reflexão, como o Guice.

Alternativas à injeção de dependência

Uma alternativa à injeção de dependência é usar um localizador de serviços. O padrão de design do localizador de serviços também melhora o desacoplamento de classes de dependências concretas. Você cria uma classe conhecida como localizador de serviços que cria e armazena dependências e as disponibiliza sob demanda.

Kotlin

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

O padrão do localizador de serviços é diferente da injeção de dependência na forma como os elementos são consumidos. Com o padrão do localizador de serviços, as classes têm controle e pedem objetos para serem injetados. Com a injeção de dependência, o app tem controle e injeta os objetos necessários de forma proativa.

Em comparação com a injeção de dependência:

  • A coleta de dependências exigida por um localizador de serviços dificulta o teste porque todos os testes precisam interagir com o mesmo localizador de serviços global.

  • As dependências são codificadas na implementação da classe, não na superfície da API. Como resultado, é mais difícil saber de fora o que uma classe precisa. Como resultado, mudanças no Car ou nas dependências disponíveis no localizador de serviços podem resultar em problemas no tempo de execução ou no teste, fazendo com que as referências falhem.

  • O gerenciamento de ciclos de vida de objetos é mais difícil se você quer ter um escopo que não seja a vida útil de todo o app.

Usar Hilt no app para Android

Hilt é a biblioteca recomendada pelo Jetpack para injeção de dependência no Android. O Hilt define uma maneira padrão de fazer a injeção de dependências (DI, na sigla em inglês) no seu aplicativo. Isso é feito com o fornecimento de contêineres para cada classe Android no projeto e gerenciamento automático dos ciclos de vida.

O Hilt foi criado com base na biblioteca de DI popular Dagger para aproveitar a correção do tempo de compilação, o desempenho do ambiente de execução, a escalonabilidade e o suporte do Android Studio fornecido pelo Dagger.

Para saber mais sobre o Hilt, consulte Injeção de dependência com Hilt.

Conclusão

Com a injeção de dependência, seu app tem as seguintes vantagens:

  • Reutilização de classes e dissociação de dependências: é mais fácil trocar implementações de uma dependência. A reutilização de código é aprimorada devido à inversão de controle, e as classes não controlam mais como as dependências são criadas. Em vez disso, funcionam com qualquer configuração.

  • Facilidade de refatoração: as dependências se tornam uma parte verificável da superfície da API para que possam ser verificadas no momento da criação do objeto ou no momento da compilação, em vez de serem ocultadas como detalhes de implementação.

  • Facilidade de teste: uma classe não gerencia suas dependências. Portanto, ao testá-la, você pode transmitir diferentes implementações para testar todos os diferentes casos.

Para entender os benefícios da injeção de dependência, você precisa testá-la manualmente no seu app, como mostrado em Injeção de dependência manual.

Outros recursos

Injeção de dependência - Wikipédia (em inglês)