Dependency Injection (DI) is a design pattern used to implement Inversion of Control (IoC) where the control of creating and managing dependencies is transferred from the application to an external entity. This helps in creating more modular, testable, and maintainable code.
It is a technique where the responsibility of creating objects is transferred to other parts of the code. This promotes loose coupling, making the code more modular and easier to manage.
Classes often need references to other classes to function properly. For instance, consider a Library
class that requires a Book
class. These necessary classes are known as dependencies. The Library
class depends on having an instance of the Book
class to operate.
There are three primary ways for a class to obtain the objects it needs:
Self-construction: The class creates and initializes its own dependencies. For example, the Library
class would create and initialize its own instance of the Book
class.
External retrieval: The class retrieves dependencies from an external source. Some Android APIs, such as Context
getters and getSystemService()
, work this way.
Dependency Injection: Dependencies are provided to the class, either when it is constructed or through methods that require them. For example, the Library
constructor would receive a Book
instance as a parameter.
The third option is dependency injection! With DI, you provide the dependencies of a class rather than having the class instance obtain them itself.
Without DI, a Library
that creates its own Book
dependency might look like this:
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();
}
}
This is not an example of DI because the Library
class constructs its own Book
. This can be problematic because:
Library
and Book
are tightly coupled. An instance of Library
uses one type of Book
, making it difficult to use subclasses or alternative implementations.Book
makes testing more challenging. Library
uses a real instance of Book
, preventing the use of test doubles to modify Book
for different test cases.With DI, instead of each instance of Library
constructing its own Book
object, it receives a Book
object as a parameter in its constructor:
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();
}
The main function uses Library
. Since Library
depends on Book
, the app creates an instance of Book
and then uses it to construct an instance of Library
. The benefits of this DI-based approach are:
Library
: You can pass in different implementations of Book
to Library
. For example, you might define a new subclass of Book
called EBook
that you want Library
to use. With DI, you simply pass an instance of EBook
to Library
, and it works without any further changes.Library
: You can pass in test doubles to test different scenarios.Consider a scenario where a NotificationService
class relies on a Notification
class. Without DI, the NotificationService
directly creates an instance of Notification
, making it difficult to use different types of notifications or to test the service with various notification implementations.
To illustrate DI, let’s refactor this example:
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();
}
}
Now, NotificationService
depends on the Notification
interface rather than a specific class. This allows different implementations of Notification
to be used interchangeably. You can set the implementation you want to use through the sendNotification
method:
NotificationService service = new NotificationService();
service.sendNotification(new EmailNotification());
service.sendNotification(new SMSNotification());
There are three main types of DI:
Method (Interface) Injection: Dependencies are passed through methods that the class can access via an interface or another class. The previous example demonstrates method injection.
Constructor Injection: Dependencies are passed to the class through its constructor.
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 (or Setter Injection): Certain Android framework classes, such as activities and fragments, are instantiated by the system, so constructor injection is not possible. With field injection, dependencies are instantiated after the class is created.
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. Method Injection: Dependencies are provided through methods, often using the @Inject
annotation.
In the previous example, you manually created, provided, and managed the dependencies of different classes without using a library. This approach is known as manual dependency injection. While it works for simple cases, it becomes cumbersome as the number of dependencies and classes increases. Manual dependency injection has several drawbacks:
Libraries can automate this process by creating and providing dependencies for you. These libraries fall into two categories:
Reflection-based Solutions: These connect dependencies at runtime.
Static Solutions: These generate code to connect dependencies at compile time.
Dagger is a popular dependency injection library for Java, Kotlin, and Android, maintained by Google. Dagger simplifies DI in your app by creating and managing the dependency graph for you. It provides fully static, compile-time dependencies, addressing many of the development and performance issues associated with reflection-based solutions like Guice.
These frameworks connect dependencies at runtime:
These frameworks generate code to connect dependencies at compile time:
Hilt: Built on top of Dagger, Hilt provides a standard way to incorporate Dagger dependency injection into an Android application. It simplifies the setup and usage of Dagger by providing predefined components and scopes.
Koin: A lightweight and simple DI framework for Kotlin. Koin uses a DSL to define dependencies and is easy to set up and use.
Kodein: A Kotlin-based DI framework that is easy to use and understand. It provides a simple and flexible API for managing dependencies.
An alternative to dependency injection is the service locator pattern. This design pattern also helps decouple classes from their concrete dependencies. You create a class known as the service locator that creates and stores dependencies, providing them on demand.
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()
}
The service locator pattern differs from dependency injection in how dependencies are consumed. With the service locator pattern, classes request the dependencies they need; with dependency injection, the app proactively provides the required objects.
Dagger 2 is a popular DI framework for Android. It uses compile-time code generation and is known for its high performance. Dagger 2 simplifies the process of dependency injection by generating the necessary code to handle dependencies, reducing boilerplate and improving efficiency.
Dagger 2 is an annotation-based library for dependency injection in Android. Here are the key annotations and their purposes:
ApiClient
for Retrofit.@Module
and @Inject
. It contains all the modules and provides the builder for the application.@Provides
but more concise.Dagger can generate a dependency graph for your project, allowing it to determine where to obtain dependencies when needed. To enable this, you need to create an interface and annotate it with @Component
.
Within the @Component
interface, you define methods that return instances of the classes you need (e.g., BookRepository
). The @Component
annotation instructs Dagger to generate a container with all the dependencies required to satisfy the types it exposes. This container is known as a Dagger component, and it contains a graph of objects that Dagger knows how to provide along with their dependencies.
Let’s consider an example involving a LibraryRepository
:
@Inject
annotation to the LibraryRepository
constructor so Dagger knows how to create an instance of 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. Annotate Dependencies: Similarly, annotate the constructors of the dependencies (LocalLibraryDataSource
and RemoteLibraryDataSource
) so Dagger knows how to create them.
public class LocalLibraryDataSource {
@Inject
public LocalLibraryDataSource() {
// Initialization code
}
}
public class RemoteLibraryDataSource {
private final LibraryService libraryService;
@Inject
public RemoteLibraryDataSource(LibraryService libraryService) {
this.libraryService = libraryService;
}
}
3. Define the Component: Create an interface annotated with @Component
to define the dependency graph.
@Component
public interface ApplicationComponent {
LibraryRepository getLibraryRepository();
}
When you build the project, Dagger generates an implementation of the ApplicationComponent
interface for you, typically named DaggerApplicationComponent
.
You can now use the generated component to obtain instances of your classes with their dependencies automatically injected:
public class MainApplication extends Application {
private ApplicationComponent applicationComponent;
@Override
public void onCreate() {
super.onCreate();
applicationComponent = DaggerApplicationComponent.create();
}
public ApplicationComponent getApplicationComponent() {
return applicationComponent;
}
}
In your activity or fragment, you can retrieve the LibraryRepository
instance:
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. Modules
∘ Key Concepts of Modules
∘ Including Modules in Components
2. Scopes
3. Components
4. Component Dependencies
5. Runtime Bindings
Modules in Dagger 2 are classes annotated with @Module
that provide dependencies to the components. They contain methods annotated with @Provides
or @Binds
to specify how to create and supply dependencies. Modules are essential for organizing and managing the creation of objects that your application needs.
@Module Annotation: This annotation is used to define a class as a Dagger module. A module class contains methods that provide dependencies.
@Provides Annotation: This annotation is used on methods within a module to indicate that the method provides a certain dependency. These methods are responsible for creating and returning instances of the dependencies.
@Binds Annotation: This annotation is used in abstract classes to bind an implementation to an interface. It is more concise than @Provides
and is used when the module is an abstract class.
Example of a Module
@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();
}
}
In this example, NetworkModule
is a class annotated with @Module
. It contains two methods annotated with @Provides
that create and return instances of Retrofit
and OkHttpClient
.
Using @Binds
When you have an interface and its implementation, you can use @Binds
to bind the implementation to the interface. This is more concise than using @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);
}
In this example, ApiModule
is an abstract class annotated with @Module
. The bindApiService
method is annotated with @Binds
to bind ApiServiceImpl
to ApiService
.
Modules can be organized based on the functionality they provide. For example, you can have separate modules for network operations, database operations, and UI-related dependencies.
Example:
Retrofit
and OkHttpClient
.RoomDatabase
.ViewModel
and Presenter
.Modules are included in components to provide dependencies to the classes that need them. Here’s how you can set it up:
ApplicationComponent.java:
@Singleton
@Component(modules = {NetworkModule.class, DatabaseModule.class})
public interface ApplicationComponent {
void inject(MyApplication application);
}
In this example, ApplicationComponent
includes NetworkModule
and DatabaseModule
to provide dependencies to the application.
Scopes in Dagger 2 are annotations that define the lifecycle of dependencies. They ensure that a single instance of a dependency is created and shared within a specified scope. This helps in managing memory efficiently and ensuring that dependencies are reused where appropriate.
1. Singleton Scope
Definition: The @Singleton
scope ensures that a single instance of a dependency is created and shared throughout the entire application’s lifecycle.
This scope is typically used for dependencies that need to be shared across the entire application, such as network clients, database instances, or shared preferences.
Example:
@Singleton
@Component(modules = {NetworkModule.class, DatabaseModule.class})
public interface ApplicationComponent {
void inject(MyApplication application);
}
In this example, the @Singleton
annotation ensures that the Retrofit
and Database
instances provided by NetworkModule
and DatabaseModule
are singletons and shared across the entire application.
2. Activity Scope
Definition: The @ActivityScope
(a custom scope) ensures that a single instance of a dependency is created and shared within the lifecycle of an activity.
This scope is useful for dependencies that are specific to an activity and should be recreated each time the activity is recreated, such as presenters or view models.
Example:
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {
}
@ActivityScope
@Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {
void inject(MainActivity mainActivity);
}
In this example, the @ActivityScope
annotation ensures that dependencies provided by ActivityModule
are scoped to the lifecycle of the activity.
3. Fragment Scope
Definition: The @FragmentScope
(another custom scope) ensures that a single instance of a dependency is created and shared within the lifecycle of a fragment.
Use Case: This scope is useful for dependencies that are specific to a fragment and should be recreated each time the fragment is recreated, such as fragment-specific presenters or view models.
Example:
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface FragmentScope {
}
@FragmentScope
@Component(dependencies = ActivityComponent.class, modules = FragmentModule.class)
public interface FragmentComponent {
void inject(MyFragment myFragment);
}
In this example, the @FragmentScope
annotation ensures that dependencies provided by FragmentModule
are scoped to the lifecycle of the fragment.
Component dependencies allow one component to depend on another, enabling the reuse of dependencies. There are two main types of component dependencies:
1. Application Component
Definition: The Application Component provides dependencies that are needed throughout the entire application. It is typically scoped with @Singleton
to ensure that the dependencies are shared across the application.
This component is used for dependencies that need to be available globally, such as network clients, database instances, or shared preferences.
Example:
@Singleton
@Component(modules = {NetworkModule.class, DatabaseModule.class})
public interface ApplicationComponent {
void inject(MyApplication application);
}
In this example, the ApplicationComponent
is responsible for providing Retrofit
and Database
instances, which are shared across the entire application.
2. Activity Component
Definition: The Activity Component provides dependencies that are needed within a specific activity. It is typically scoped with a custom scope, such as @ActivityScope
, to ensure that the dependencies are recreated each time the activity is recreated.
This component is used for dependencies that are specific to an activity, such as presenters or view models.
Example:
@ActivityScope
@Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {
void inject(MainActivity mainActivity);
}
In this example, the ActivityComponent
depends on the ApplicationComponent
and provides dependencies specific to the MainActivity
.
Component dependencies allow one component to depend on another, enabling the reuse of dependencies. There are two main types of component dependencies:
1. Subcomponents:
A subcomponent is a child of another component and can access its parent’s dependencies. Subcomponents are defined within the parent component and can inherit its scope.
Example:
@ActivityScope
@Subcomponent(modules = ActivityModule.class)
public interface ActivitySubcomponent {
void inject(MainActivity mainActivity);
}
In this example, ActivitySubcomponent
is a subcomponent of the parent component and can access its dependencies.
2. Dependency Attribute
This allows a component to depend on another component without being a subcomponent. The dependent component can access the dependencies provided by the parent component.
Example:
@ActivityScope
@Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {
void inject(MainActivity mainActivity);
}
In this example, ActivityComponent
depends on ApplicationComponent
and can access its dependencies.
Runtime bindings in Dagger 2 refer to the provision of dependencies that are created and managed at runtime, based on the context in which they are needed.
1. Application Context
Definition: The application context is a context that is tied to the lifecycle of the entire application. It is used for dependencies that need to live as long as the application itself.
Dependencies are shared across the entire application and do not need to be recreated for each activity or fragment. Examples include network clients, database instances, and shared preferences.
Example:
@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();
}
}
In this example, AppModule
provides the application context as a singleton dependency. The provideApplicationContext
method ensures that the context provided is tied to the lifecycle of the application.
2. Activity Context
Definition: The activity context is a context that is tied to the lifecycle of a specific activity. It is used for dependencies that need to live as long as the activity itself.
Dependencies that are specific to an activity and should be recreated each time the activity is recreated. Examples include view models, presenters, and UI-related dependencies.
Example:
@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;
}
}
In this example, ActivityModule
provides the activity context as a scoped dependency. The provideActivityContext
method ensures that the context provided is tied to the lifecycle of the activity.
To use these runtime bindings, you need to include the corresponding modules in your components:
Application Component:
@Singleton
@Component(modules = {AppModule.class, NetworkModule.class})
public interface ApplicationComponent {
void inject(MyApplication application);
Context getApplicationContext();
}
Activity Component:
@ActivityScope
@Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {
void inject(MainActivity mainActivity);
Context getActivityContext();
}
Injecting Contexts
Once you have set up your components and modules, you can inject the contexts into your classes as needed.
Example in an Activity:
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);
}
}
In this example, MainActivity
receives both the activity context and the application context through dependency injection. This allows the activity to use the appropriate context based on the specific needs of the dependencies.
To use Dagger 2 in your project, you need to add the following dependencies to your build.gradle
file:
dependencies {
implementation 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}
Replace 2.x
with the latest version of Dagger 2.
Create a module to provide dependencies. For example, a NetworkModule
to provide a Retrofit
instance:
@Module
public class NetworkModule {
@Provides
@Singleton
Retrofit provideRetrofit() {
return new Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
}
}
Create a component to bridge the module and the classes that need the dependencies:
@Singleton
@Component(modules = {NetworkModule.class})
public interface ApplicationComponent {
void inject(MyApplication application);
}
Use the component to inject dependencies into your classes. For example, in your Application
class:
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;
}
}
Now, you can use the injected dependencies in your classes. For example, in an 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
// ...
}
}
Let’s summarize this topic: