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:
Library
criaria e inicializaria sua própria instância da classe Book
.Context
getters e getSystemService()
, funcionam dessa forma.Library
receberia uma instância Book
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.
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:
Library
e Book
são acoplados forte. Uma instância de Library
usa um tipo de Book
, dificultando o uso de subclasses ou implementações alternativas.Book
torna o teste mais desafiador. Library
usa uma instância real de Book
, impedindo o uso de dublês de teste para modificar Book
para diferentes casos de teste. 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:
Library
: Você pode passar diferentes implementações de Book
para Library
. Por exemplo, você pode definir uma nova subclasse de Book
chamada EBook
que você quer que Library
use. Com DI, você simplesmente passa uma instância de EBook
para Library
, e funciona sem nenhuma outra alteração.Library
: você pode passar testes duplos para testar diferentes cenários. 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());
Existem três tipos principais de DI:
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
.
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:
As bibliotecas podem automatizar esse processo criando e fornecendo dependências para você. Essas bibliotecas se dividem em duas categorias:
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.
Essas estruturas conectam dependências em tempo de execução:
Essas estruturas geram código para conectar dependências em tempo de compilação:
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.
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:
ApiClient
para Retrofit.@Module
e @Inject
. Ela contém todos os módulos e fornece o construtor para o aplicativo.@Provides
mas mais conciso. 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.
Vamos considerar um exemplo envolvendo um LibraryRepository
:
@Inject
ao construtor LibraryRepository
para que o Dagger saiba como criar uma instância de LibraryRepository
. 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
.
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 } }
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
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.
@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:
Retrofit
e OkHttpClient
.RoomDatabase
.ViewModel
e Presenter
.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.
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.
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.
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:
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
.
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:
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.
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.
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.
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.
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.
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(); } }
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); }
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; } }
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 // ... } }
Vamos resumir este tópico: