Introdução à injeção de dependência
Dependency Injection (DI) é um padrão de design usado para implementar Inversion of Control (IoC) onde o controle de criação e gerenciamento de dependências é transferido do aplicativo para uma entidade externa. Isso ajuda a criar um código mais modular, testável e sustentável. É uma técnica onde a responsabilidade de criar objetos é transferida para outras partes do código. Isso promove acoplamento frouxo, tornando o código mais modular e mais fácil de gerenciar.
As classes geralmente precisam de referências a outras classes para funcionar corretamente. Por exemplo, considere uma classe Library
que requer uma classe Book
. Essas classes necessárias são conhecidas como dependências. A classe Library
depende de ter uma instância da classe Book
para operar.
Há três maneiras principais para uma classe obter os objetos de que necessita:
- Autoconstrução : A classe cria e inicializa suas próprias dependências. Por exemplo, a classe
Library
criaria e inicializaria sua própria instância da classeBook
. - Recuperação externa : A classe recupera dependências de uma fonte externa. Algumas APIs do Android, como
Context
getters egetSystemService()
, funcionam dessa forma. - Injeção de dependência : Dependências são fornecidas à classe, seja quando ela é construída ou por meio de métodos que as requerem. Por exemplo, o construtor
Library
receberia uma instânciaBook
como parâmetro.
A terceira opção é injeção de dependência! Com DI, você fornece as dependências de uma classe em vez de fazer com que a instância da classe as obtenha ela mesma.
Exemplo sem injeção de dependência
Sem DI, uma Library
que cria sua própria dependência Book
pode se parecer com isto:
class Library { private Book book = new Book(); void open() { book.read(); } } public class Main { public static void main(String[] args) { Library library = new Library(); library.open(); } }
Este não é um exemplo de DI porque a classe Library
constrói seu próprio Book
. Isso pode ser problemático porque:
- Acoplamento forte :
Library
eBook
são acoplados forte. Uma instância deLibrary
usa um tipo deBook
, dificultando o uso de subclasses ou implementações alternativas. - Dificuldades de teste : A dependência hard em
Book
torna o teste mais desafiador.Library
usa uma instância real deBook
, impedindo o uso de dublês de teste para modificarBook
para diferentes casos de teste.
Exemplo com injeção de dependência
Com DI, em vez de cada instância de Library
construir seu próprio objeto Book
, ela recebe um objeto Book
como parâmetro em seu construtor:
class Library { private Book book; Library(Book book) { this.book = book; } void open() { book.read(); } } public class Main { public static void main(String[] args) { Book book = new Book(); Library library = new Library(book); library.open(); }
A função principal usa Library
. Como Library
depende de Book
, o aplicativo cria uma instância de Book
e a usa para construir uma instância de Library
. Os benefícios dessa abordagem baseada em DI são:
- Reusabilidade de
Library
: Você pode passar diferentes implementações deBook
paraLibrary
. Por exemplo, você pode definir uma nova subclasse deBook
chamadaEBook
que você quer queLibrary
use. Com DI, você simplesmente passa uma instância deEBook
paraLibrary
, e funciona sem nenhuma outra alteração. - Teste fácil da
Library
: você pode passar testes duplos para testar diferentes cenários.
Outro exemplo de DI
Considere um cenário em que uma classe NotificationService
depende de uma classe Notification
. Sem DI, o NotificationService
cria diretamente uma instância de Notification
, dificultando o uso de diferentes tipos de notificações ou o teste do serviço com várias implementações de notificação.
Para ilustrar DI, vamos refatorar este exemplo:
interface Notification { void send(); } class EmailNotification implements Notification { @Override public void send() { // Send email notification } } class SMSNotification implements Notification { @Override public void send() { // Send SMS notification } } class NotificationService { void sendNotification(Notification notification) { notification.send(); } }
Agora, NotificationService
depende da interface Notification
em vez de uma classe específica. Isso permite que diferentes implementações de Notification
sejam usadas de forma intercambiável. Você pode definir a implementação que deseja usar por meio do método sendNotification
:
NotificationService service = new NotificationService(); service.sendNotification(new EmailNotification()); service.sendNotification(new SMSNotification());
Métodos de injeção de dependência no Android
Existem três tipos principais de DI:
- Injeção de Método (Interface) : Dependências são passadas por métodos que a classe pode acessar por meio de uma interface ou outra classe. O exemplo anterior demonstra injeção de método.
- Injeção de Construtor : Dependências são passadas para a classe através de seu construtor.
class NotificationService { private final Notification notification; public NotificationService(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(new EmailNotification()); service.sendNotification(); } }
3. Field Injection (ou Setter Injection) : Certas classes do framework Android, como activities e fragments, são instanciadas pelo sistema, então a injeção de construtor não é possível. Com a injeção de campo, as dependências são instanciadas após a classe ser criada.
class NotificationService { private Notification notification; public Notification getNotification() { return notification; } public void setNotification(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(); service.setNotification(new EmailNotification()); service.sendNotification(); } }
4. Injeção de método : as dependências são fornecidas por meio de métodos, geralmente usando a anotação @Inject
.
Vantagens da injeção de dependência
- As classes se tornam mais reutilizáveis e menos dependentes de implementações específicas. Isso se deve à inversão de controle, onde as classes não gerenciam mais suas dependências, mas trabalham com qualquer configuração fornecida.
- Dependências fazem parte da superfície da API e podem ser verificadas na criação do objeto ou no momento da compilação, facilitando a refatoração.
- Como uma classe não gerencia suas dependências, diferentes implementações podem ser passadas durante os testes para cobrir vários cenários.
Injeção de Dependência Automatizada
No exemplo anterior, você criou, forneceu e gerenciou manualmente as dependências de diferentes classes sem usar uma biblioteca. Essa abordagem é conhecida como injeção manual de dependência. Embora funcione para casos simples, torna-se trabalhoso à medida que o número de dependências e classes aumenta. A injeção manual de dependência tem várias desvantagens:
- Boilerplate Code : Para grandes aplicativos, gerenciar todas as dependências e conectá-las corretamente pode resultar em muito código repetitivo. Em uma arquitetura multicamadas, criar um objeto para uma camada superior requer fornecer todas as dependências para as camadas abaixo dela. Por exemplo, para construir um computador, você precisa de uma CPU, uma placa-mãe, RAM e outros componentes; e uma CPU pode precisar de transistores e capacitores.
- Gerenciamento de dependências complexas : quando não é possível construir dependências antecipadamente — como com inicializações lentas ou escopo de objetos para fluxos específicos no seu aplicativo — você precisa escrever e manter um contêiner personalizado (ou gráfico de dependências) para gerenciar o tempo de vida das suas dependências na memória.
As bibliotecas podem automatizar esse processo criando e fornecendo dependências para você. Essas bibliotecas se dividem em duas categorias:
- Soluções baseadas em reflexão : conectam dependências em tempo de execução.
- Soluções estáticas : geram código para conectar dependências em tempo de compilação.
Dagger é uma biblioteca popular de injeção de dependência para Java, Kotlin e Android, mantida pelo Google. Dagger simplifica DI em seu aplicativo criando e gerenciando o gráfico de dependência para você. Ele fornece dependências totalmente estáticas e em tempo de compilação, abordando muitos dos problemas de desenvolvimento e desempenho associados a soluções baseadas em reflexão como Guice.
Soluções baseadas em reflexão
Essas estruturas conectam dependências em tempo de execução:
- Toothpick : Um framework de DI de tempo de execução que usa reflexão para conectar dependências. Ele foi projetado para ser leve e rápido, tornando-o adequado para aplicativos Android.
Soluções Estáticas
Essas estruturas geram código para conectar dependências em tempo de compilação:
- Hilt : Construído sobre o Dagger, o Hilt fornece uma maneira padrão de incorporar injeção de dependência do Dagger em um aplicativo Android. Ele simplifica a configuração e o uso do Dagger ao fornecer componentes e escopos predefinidos .
- Koin : Um framework DI leve e simples para Kotlin. Koin usa um DSL para definir dependências e é fácil de configurar e usar.
- Kodein : Um framework DI baseado em Kotlin que é fácil de usar e entender. Ele fornece uma API simples e flexível para gerenciar dependências.
Alternativas à injeção de dependência
Uma alternativa à injeção de dependência é o padrão service locator. Esse padrão de design também ajuda a desacoplar classes de suas dependências concretas. Você cria uma classe conhecida como service locator que cria e armazena dependências, fornecendo-as sob demanda.
object ServiceLocator { fun getProcessor(): Processor = Processor() } class Computer { private val processor = ServiceLocator.getProcessor() fun start() { processor.run() } } fun main(args: Array<String>) { val computer = Computer() computer.start() }
O padrão do localizador de serviço difere da injeção de dependência em como as dependências são consumidas. Com o padrão do localizador de serviço, as classes solicitam as dependências de que precisam; com a injeção de dependência, o aplicativo fornece proativamente os objetos necessários.
O que é Dagger 2?
Dagger 2 é um framework DI popular para Android. Ele usa geração de código em tempo de compilação e é conhecido por seu alto desempenho. O Dagger 2 simplifica o processo de injeção de dependência gerando o código necessário para lidar com dependências, reduzindo boilerplate e melhorando a eficiência.
Dagger 2 é uma biblioteca baseada em anotação para injeção de dependência no Android. Aqui estão as principais anotações e seus propósitos:
- @Module : Usado para definir classes que fornecem dependências. Por exemplo, um módulo pode fornecer um
ApiClient
para Retrofit. - @Provides : Anota métodos em um módulo para especificar como criar e retornar dependências.
- @Inject : Usado para solicitar dependências. Pode ser aplicado a campos, construtores e métodos.
- @Component : Uma interface que faz a ponte entre
@Module
e@Inject
. Ela contém todos os módulos e fornece o construtor para o aplicativo. - @Singleton : Garante que uma única instância de uma dependência seja criada.
- @Binds : Usado em classes abstratas para fornecer dependências, semelhante a
@Provides
mas mais conciso.
Componentes de Adaga
O Dagger pode gerar um gráfico de dependências para seu projeto, permitindo que ele determine onde obter dependências quando necessário. Para habilitar isso, você precisa criar uma interface e anotá-la com @Component
.
Dentro da interface @Component
, você define métodos que retornam instâncias das classes que você precisa (por exemplo, BookRepository
). A anotaçã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. Esse contêiner é conhecido como um componente Dagger e contém um gráfico de objetos que o Dagger sabe como fornecer junto com suas dependências.
Exemplo
Vamos considerar um exemplo envolvendo um LibraryRepository
:
- Anotar o construtor : adicione uma anotação
@Inject
ao construtorLibraryRepository
para que o Dagger saiba como criar uma instância deLibraryRepository
.
public class LibraryRepository { private final LocalLibraryDataSource localDataSource; private final RemoteLibraryDataSource remoteDataSource; @Inject public LibraryRepository(LocalLibraryDataSource localDataSource, RemoteLibraryDataSource remoteDataSource) { this.localDataSource = localDataSource; this.remoteDataSource = remoteDataSource; } }
2. Anotar dependências : Da mesma forma, anote os construtores das dependências ( LocalLibraryDataSource
e RemoteLibraryDataSource
) para que o Dagger saiba como criá-las.
public class LocalLibraryDataSource { @Inject public LocalLibraryDataSource() { // Initialization code } } public class RemoteLibraryDataSource { private final LibraryService libraryService; @Inject public RemoteLibraryDataSource(LibraryService libraryService) { this.libraryService = libraryService; } }
3. Defina o componente : crie uma interface anotada com @Component
para definir o gráfico de dependência.
@Component public interface ApplicationComponent { LibraryRepository getLibraryRepository(); }
Quando você cria o projeto, o Dagger gera uma implementação da interface ApplicationComponent
para você, normalmente chamada DaggerApplicationComponent
.
Uso
Agora você pode usar o componente gerado para obter instâncias de suas classes com suas dependências injetadas automaticamente:
public class MainApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.create(); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
Na sua atividade ou fragmento, você pode recuperar a instância LibraryRepository
:
public class MainActivity extends AppCompatActivity { @Inject LibraryRepository libraryRepository; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MainApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected libraryRepository } }
Conceitos-chave em Dagger 2
1. Módulos
∘ Conceitos-chave dos módulos
∘ Incluindo Módulos em Componentes
2. Escopos
3. Componentes
4. Dependências de componentes
5. Ligações de tempo de execução
1. Módulos
Módulos no Dagger 2 são classes anotadas com @Module
que fornecem dependências para os componentes. Eles contêm métodos anotados com @Provides
ou @Binds
para especificar como criar e fornecer dependências. Módulos são essenciais para organizar e gerenciar a criação de objetos que seu aplicativo precisa.
Conceitos-chave dos módulos
- @Anotação de módulo: Esta anotação é usada para definir uma classe como um módulo Dagger. Uma classe de módulo contém métodos que fornecem dependências.
- @Provides Annotation: Esta anotação é usada em métodos dentro de um módulo para indicar que o método fornece uma certa dependência. Esses métodos são responsáveis por criar e retornar instâncias das dependências.
- Anotação @Binds: Esta anotação é usada em classes abstratas para vincular uma implementação a uma interface. Ela é mais concisa que
@Provides
e é usada quando o módulo é uma classe abstrata.
Exemplo de um módulo
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } @Provides @Singleton OkHttpClient provideOkHttpClient() { return new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build(); } }
Neste exemplo, NetworkModule
é uma classe anotada com @Module
. Ela contém dois métodos anotados com @Provides
que criam e retornam instâncias de Retrofit
e OkHttpClient
.
Usando @Binds
Quando você tem uma interface e sua implementação, você pode usar @Binds
para vincular a implementação à interface. Isso é mais conciso do que usar @Provides
.
public interface ApiService { void fetchData(); } public class ApiServiceImpl implements ApiService { @Override public void fetchData() { // Implementation } } @Module public abstract class ApiModule { @Binds abstract ApiService bindApiService(ApiServiceImpl apiServiceImpl); }
Neste exemplo, ApiModule
é uma classe abstrata anotada com @Module
. O método bindApiService
é anotado com @Binds
para vincular ApiServiceImpl
a ApiService
.
Os módulos podem ser organizados com base na funcionalidade que eles fornecem. Por exemplo, você pode ter módulos separados para operações de rede, operações de banco de dados e dependências relacionadas à UI.
Exemplo:
- NetworkModule : fornece dependências relacionadas à rede, como
Retrofit
eOkHttpClient
. - DatabaseModule : fornece dependências relacionadas ao banco de dados, como
RoomDatabase
. - UIModule : fornece dependências relacionadas à interface do usuário, como
ViewModel
ePresenter
.
Incluindo módulos em componentes
Os módulos são incluídos em componentes para fornecer dependências às classes que precisam deles. Veja como você pode configurá-lo:
ApplicationComponent.java:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Neste exemplo, ApplicationComponent
inclui NetworkModule
e DatabaseModule
para fornecer dependências ao aplicativo.
2. Escopos
Os escopos no Dagger 2 são anotações que definem o ciclo de vida das dependências. Eles garantem que uma única instância de uma dependência seja criada e compartilhada dentro de um escopo especificado. Isso ajuda a gerenciar a memória de forma eficiente e a garantir que as dependências sejam reutilizadas quando apropriado.
- Escopo Singleton : garante uma única instância de uma dependência durante todo o ciclo de vida do aplicativo.
- Escopo da atividade : garante uma única instância de uma dependência dentro do ciclo de vida de uma atividade.
- Escopo do fragmento : garante uma única instância de uma dependência dentro do ciclo de vida de um fragmento.
1. Escopo Singleton
Definição : O escopo @Singleton
garante que uma única instância de uma dependência seja criada e compartilhada durante todo o ciclo de vida do aplicativo.
Esse escopo normalmente é usado para dependências que precisam ser compartilhadas por todo o aplicativo, como clientes de rede, instâncias de banco de dados ou preferências compartilhadas.
Exemplo:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Neste exemplo, a anotação @Singleton
garante que as instâncias Retrofit
e Database
fornecidas pelo NetworkModule
e DatabaseModule
sejam singletons e compartilhadas por todo o aplicativo.
2. Âmbito da atividade
Definição : O @ActivityScope
(um escopo personalizado) garante que uma única instância de uma dependência seja criada e compartilhada dentro do ciclo de vida de uma atividade.
Esse escopo é útil para dependências específicas de uma atividade e devem ser recriadas sempre que a atividade for recriada, como apresentadores ou modelos de exibição.
Exemplo :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope { } @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
Neste exemplo, a anotação @ActivityScope
garante que as dependências fornecidas pelo ActivityModule
sejam delimitadas ao ciclo de vida da atividade.
3. Escopo do fragmento
Definição : O @FragmentScope
(outro escopo personalizado) garante que uma única instância de uma dependência seja criada e compartilhada dentro do ciclo de vida de um fragmento.
Caso de uso: esse escopo é útil para dependências específicas de um fragmento e devem ser recriadas sempre que o fragmento for recriado, como apresentadores específicos de fragmento ou modelos de exibição.
Exemplo :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScope { } @FragmentScope @Component(dependencies = ActivityComponent.class, modules = FragmentModule.class) public interface FragmentComponent { void inject(MyFragment myFragment); }
Neste exemplo, a anotação @FragmentScope
garante que as dependências fornecidas pelo FragmentModule
sejam delimitadas ao ciclo de vida do fragmento.
3. Componentes
Dependências de componentes permitem que um componente dependa de outro, permitindo a reutilização de dependências. Existem dois tipos principais de dependências de componentes:
- Componente do aplicativo : fornece dependências necessárias em todo o aplicativo.
- Componente de atividade : fornece dependências necessárias dentro de uma atividade específica.
1. Componente de Aplicação
Definição : O Application Component fornece dependências que são necessárias em todo o aplicativo. Ele é tipicamente delimitado com @Singleton
para garantir que as dependências sejam compartilhadas em todo o aplicativo.
Este componente é usado para dependências que precisam estar disponíveis globalmente, como clientes de rede, instâncias de banco de dados ou preferências compartilhadas.
Exemplo :
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Neste exemplo, o ApplicationComponent
é responsável por fornecer instâncias de Retrofit
e Database
, que são compartilhadas por todo o aplicativo.
2. Componente de atividade
Definição : O Activity Component fornece dependências que são necessárias dentro de uma atividade específica. Ele é tipicamente delimitado com um escopo personalizado, como @ActivityScope
, para garantir que as dependências sejam recriadas sempre que a atividade for recriada.
Este componente é usado para dependências específicas de uma atividade, como apresentadores ou modelos de exibição.
Exemplo :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
Neste exemplo, o ActivityComponent
depende do ApplicationComponent
e fornece dependências específicas para o MainActivity
.
4. Dependências de componentes
Dependências de componentes permitem que um componente dependa de outro, permitindo a reutilização de dependências. Existem dois tipos principais de dependências de componentes:
- Subcomponentes : Um subcomponente é filho de outro componente e pode acessar as dependências de seu pai.
- Atributo de dependência : permite que um componente dependa de outro componente sem ser um subcomponente.
1. Subcomponentes:
Um subcomponente é filho de outro componente e pode acessar as dependências de seu pai. Subcomponentes são definidos dentro do componente pai e podem herdar seu escopo.
Exemplo :
@ActivityScope @Subcomponent(modules = ActivityModule.class) public interface ActivitySubcomponent { void inject(MainActivity mainActivity); }
Neste exemplo, ActivitySubcomponent
é um subcomponente do componente pai e pode acessar suas dependências.
2. Atributo de dependência
Isso permite que um componente dependa de outro componente sem ser um subcomponente. O componente dependente pode acessar as dependências fornecidas pelo componente pai.
Exemplo :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
Neste exemplo, ActivityComponent
depende de ApplicationComponent
e pode acessar suas dependências.
5. Ligações de tempo de execução
As vinculações de tempo de execução no Dagger 2 referem-se ao fornecimento de dependências que são criadas e gerenciadas em tempo de execução, com base no contexto em que são necessárias.
- Contexto do aplicativo : usado para dependências que precisam durar tanto quanto o aplicativo.
- Contexto da atividade : usado para dependências que precisam durar tanto quanto uma atividade.
1. Contexto da aplicação
Definição : O contexto do aplicativo é um contexto que está vinculado ao ciclo de vida de todo o aplicativo. Ele é usado para dependências que precisam viver tanto quanto o próprio aplicativo.
Dependências que são compartilhadas por todo o aplicativo e não precisam ser recriadas para cada atividade ou fragmento. Exemplos incluem clientes de rede, instâncias de banco de dados e preferências compartilhadas.
Exemplo :
@Module public class AppModule { private final Application application; public AppModule(Application application) { this.application = application; } @Provides @Singleton Application provideApplication() { return application; } @Provides @Singleton Context provideApplicationContext() { return application.getApplicationContext(); } }
Neste exemplo, AppModule
fornece o contexto do aplicativo como uma dependência singleton. O método provideApplicationContext
garante que o contexto fornecido esteja vinculado ao ciclo de vida do aplicativo.
2. Contexto da atividade
Definição : O contexto de atividade é um contexto que está vinculado ao ciclo de vida de uma atividade específica. Ele é usado para dependências que precisam viver tanto quanto a atividade em si.
Dependências que são específicas de uma atividade e devem ser recriadas sempre que a atividade for recriada. Exemplos incluem modelos de visualização, apresentadores e dependências relacionadas à IU.
Exemplo :
@Module public class ActivityModule { private final Activity activity; public ActivityModule(Activity activity) { this.activity = activity; } @Provides @ActivityScope Activity provideActivity() { return activity; } @Provides @ActivityScope Context provideActivityContext() { return activity; } }
Neste exemplo, ActivityModule
fornece o contexto da atividade como uma dependência com escopo. O método provideActivityContext
garante que o contexto fornecido esteja vinculado ao ciclo de vida da atividade.
Usando ligações de tempo de execução em componentes
Para usar essas vinculações de tempo de execução, você precisa incluir os módulos correspondentes em seus componentes:
Componente do aplicativo :
@Singleton @Component(modules = {AppModule.class, NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); Context getApplicationContext(); }
Componente de atividade :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); Context getActivityContext(); }
Injetando Contextos
Depois de configurar seus componentes e módulos, você pode injetar os contextos em suas classes conforme necessário.
Exemplo em uma atividade :
public class MainActivity extends AppCompatActivity { @Inject Context activityContext; @Inject Context applicationContext; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ApplicationComponent appComponent = ((MyApplication) getApplication()).getApplicationComponent(); ActivityComponent activityComponent = DaggerActivityComponent.builder() .applicationComponent(appComponent) .activityModule(new ActivityModule(this)) .build(); activityComponent.inject(this); // Use the injected contexts Log.d("MainActivity", "Activity Context: " + activityContext); Log.d("MainActivity", "Application Context: " + applicationContext); } }
Neste exemplo, MainActivity
recebe tanto o contexto da atividade quanto o contexto do aplicativo por meio de injeção de dependência. Isso permite que a atividade use o contexto apropriado com base nas necessidades específicas das dependências.
Exemplo: Usando Dagger 2 em um aplicativo Android
Configurando Dagger 2
Para usar o Dagger 2 em seu projeto, você precisa adicionar as seguintes dependências ao seu arquivo build.gradle
:
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Substitua 2.x
pela versão mais recente do Dagger 2.
Etapa 1: Definir um módulo
Crie um módulo para fornecer dependências. Por exemplo, um NetworkModule
para fornecer uma instância Retrofit
:
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } }
Etapa 2: Definir um componente
Crie um componente para conectar o módulo e as classes que precisam das dependências:
@Singleton @Component(modules = {NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Etapa 3: Injetar dependências
Use o componente para injetar dependências em suas classes. Por exemplo, em sua classe Application
:
public class MyApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.builder() .networkModule(new NetworkModule()) .build(); applicationComponent.inject(this); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
Etapa 4: Use dependências injetadas
Agora você pode usar as dependências injetadas em suas classes. Por exemplo, em uma Activity
:
public class MainActivity extends AppCompatActivity { @Inject Retrofit retrofit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MyApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected Retrofit instance // ... } }
Conclusão
Vamos resumir este tópico:
- O principal objetivo do DI é afrouxar o acoplamento, facilitando o gerenciamento de dependências.
- Ao usar DI, você pode aumentar a flexibilidade do código e simplificar o processo de teste.
- DI é um tópico complexo com diferentes implementações baseadas no cenário.
- A DI em diferentes idiomas tem peculiaridades que podem afetar a maneira como você trabalha com ela.
- O Dagger 2 automatiza o processo de criação e fornecimento de dependências, reduzindo o código clichê e melhorando a manutenibilidade.
- O Dagger 2 oferece segurança em tempo de compilação, garantindo que todas as dependências sejam satisfeitas antes da execução do aplicativo.
- Ao gerar código em tempo de compilação, o Dagger 2 evita a sobrecarga de desempenho associada a soluções baseadas em reflexão.