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:
- A classe cria a dependência necessária. No exemplo acima,
Car
criaria e inicializaria a própria instância deEngine
. - Pegue-o de outro lugar. Algumas APIs do Android, como getters
Context
egetSystemService()
, funcionam dessa maneira. - 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
receberiaEngine
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(); } }
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
eEngine
estão fortemente acoplados. Uma instância deCar
usa um tipo deEngine
, e nenhuma subclasse ou implementação alternativa pode ser facilmente usada. Se oCar
construísse o próprioEngine
, você precisaria criar dois tipos deCar
em vez de reutilizar o mesmoCar
para motores dos tiposGas
eElectric
.A forte dependência de
Engine
dificulta os testes.Car
usa uma instância real deEngine
. Isso evita o uso de um teste duplo para modificarEngine
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, ela 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(); } }
A função main
usa Car
. Como Car
depende de 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 deEngine
paraCar
. Por exemplo, é possível definir uma nova subclasse deEngine
chamadaElectricEngine
que você quer queCar
use. Se usar a injeção de dependência, tudo o que você precisará fazer é transmitir uma instância da subclasseElectricEngine
atualizada, eCar
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 deEngine
chamadoFakeEngine
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 boilerplate. 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 na vida real, você precisa de um motor, uma transmissão, um chassi e outras peças. O motor, por sua vez, precisa de cilindros e velas de ignição.
Quando não é 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ê precisa 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 momento da execução.
Soluções estáticas que geram o código para conectar dependências no momento da compilação.
O Dagger é uma biblioteca popular de injeção de dependências 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ências
Uma alternativa à injeção de dependências é 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 o Hilt no app Android
O Hilt é a biblioteca recomendada pelo Jetpack para injeção de dependências 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 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 conhecida biblioteca de DI 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 fornecidos pelo Dagger.
Para saber mais sobre o Hilt, consulte Injeção de dependências com o 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 conferidas 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 testes: uma classe não gerencia as próprias dependências. Portanto, ao fazer um teste, você pode transmitir diferentes implementações para verificar todos os diferentes casos.
Para entender os benefícios da injeção de dependências, você precisa fazer testes manuais no seu app, como mostrado em Injeção de dependências manual.
Outros recursos
Para saber mais sobre injeção de dependências, consulte os recursos a seguir.