This content originally appeared on HackerNoon and was authored by Dilip Patel
\
Introduction to Dependency Injection
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 theBook
class. - External retrieval: The class retrieves dependencies from an external source. Some Android APIs, such as
Context
getters andgetSystemService()
, 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 aBook
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.
Example Without Dependency Injection
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:
- Tight coupling:
Library
andBook
are tightly coupled. An instance ofLibrary
uses one type ofBook
, making it difficult to use subclasses or alternative implementations. - Testing difficulties: The hard dependency on
Book
makes testing more challenging.Library
uses a real instance ofBook
, preventing the use of test doubles to modifyBook
for different test cases.
Example With Dependency Injection
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:
- Reusability of
Library
: You can pass in different implementations ofBook
toLibrary
. For example, you might define a new subclass ofBook
calledEBook
that you wantLibrary
to use. With DI, you simply pass an instance ofEBook
toLibrary
, and it works without any further changes. - Easy testing of
Library
: You can pass in test doubles to test different scenarios.
Another DI Example
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());
Methods of Dependency Injection in Android
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.
Advantages of Dependency Injection
- Classes become more reusable and less dependent on specific implementations. This is due to the inversion of control, where classes no longer manage their dependencies but work with any configuration provided.
- Dependencies are part of the API surface and can be verified at object creation or compile time, making refactoring easier.
- Since a class does not manage its dependencies, different implementations can be passed in during testing to cover various scenarios.
Automated Dependency Injection
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:
- Boilerplate Code: For large applications, managing all dependencies and connecting them correctly can result in a lot of repetitive code. In a multi-layered architecture, creating an object for a top layer requires providing all dependencies for the layers below it. For instance, to build a computer, you need a CPU, a motherboard, RAM, and other components; and a CPU might need transistors and capacitors.
- Complex Dependency Management: When you can’t construct dependencies beforehand — such as with lazy initializations or scoping objects to specific flows in your app — you need to write and maintain a custom container (or dependency graph) to manage the lifetimes of your dependencies in memory.
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.
Reflection-based Solutions
These frameworks connect dependencies at runtime:
- Toothpick: A runtime DI framework that uses reflection to connect dependencies. It’s designed to be lightweight and fast, making it suitable for Android applications.
Static Solutions
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.
Alternatives to Dependency Injection
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.
What is Dagger 2?
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:
- @Module: Used to define classes that provide dependencies. For example, a module can provide an
ApiClient
for Retrofit. - @Provides: Annotates methods in a module to specify how to create and return dependencies.
- @Inject: Used to request dependencies. Can be applied to fields, constructors, and methods.
- @Component: An interface that bridges
@Module
and@Inject
. It contains all the modules and provides the builder for the application. - @Singleton: Ensures a single instance of a dependency is created.
- @Binds: Used in abstract classes to provide dependencies, similar to
@Provides
but more concise.
Dagger Components
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.
\
Example
Let’s consider an example involving a LibraryRepository
:
- Annotate the Constructor: Add an
@Inject
annotation to theLibraryRepository
constructor so Dagger knows how to create an instance ofLibraryRepository
.
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
.
Usage
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
}
}
Key Concepts in Dagger 2
1. Modules \n ∘ Key Concepts of Modules \n ∘ Including Modules in Components \n 2. Scopes \n 3. Components \n 4. Component Dependencies \n 5. Runtime Bindings
1. Modules
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.
Key Concepts of Modules
- @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:
- NetworkModule: Provides network-related dependencies like
Retrofit
andOkHttpClient
. - DatabaseModule: Provides database-related dependencies like
RoomDatabase
. - UIModule: Provides UI-related dependencies like
ViewModel
andPresenter
.
Including Modules in Components
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.
2. Scopes
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.
- Singleton Scope: Ensures a single instance of a dependency throughout the application’s lifecycle.
- Activity Scope: Ensures a single instance of a dependency within the lifecycle of an activity.
- Fragment Scope: Ensures a single instance of a dependency within the lifecycle of a fragment.
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.
3. Components
Component dependencies allow one component to depend on another, enabling the reuse of dependencies. There are two main types of component dependencies:
- Application Component: Provides dependencies that are needed throughout the entire application.
- Activity Component: Provides dependencies that are needed within a specific activity.
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
.
4. Component Dependencies
Component dependencies allow one component to depend on another, enabling the reuse of dependencies. There are two main types of component dependencies:
- Subcomponents: A subcomponent is a child of another component and can access its parent’s dependencies.
- Dependency Attribute: This allows a component to depend on another component without being a subcomponent.
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.
5. Runtime Bindings
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.
- Application Context: Used for dependencies that need to live as long as the application.
- Activity Context: Used for dependencies that need to live as long as an activity.
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 that 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.
Using Runtime Bindings in Components
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.
Example: Using Dagger 2 in an Android Application
Setting Up Dagger 2
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.
Step 1: Define a Module
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();
}
}
Step 2: Define a Component
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);
}
Step 3: Inject Dependencies
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;
}
}
Step 4: Use Injected Dependencies
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
// ...
}
}
Conclusion
Let’s summarize this topic:
- The main point of DI is to loosen coupling, making it easier to manage dependencies.
- By using DI, you can increase code flexibility and simplify the testing process.
- DI is a complex topic with different implementations based on the scenario.
- DI in different languages has peculiarities that can affect how you work with it.
- Dagger 2 automates the process of creating and providing dependencies, reducing boilerplate code and improving maintainability.
- Dagger 2 provides compile-time safety, ensuring that all dependencies are satisfied before the application runs.
- By generating code at compile time, Dagger 2 avoids the performance overhead associated with reflection-based solutions.
\
This content originally appeared on HackerNoon and was authored by Dilip Patel
Dilip Patel | Sciencx (2024-08-28T12:00:25+00:00) Dependency Injection With Dagger 2: What Is It, Key Concepts, and More. Retrieved from https://www.scien.cx/2024/08/28/dependency-injection-with-dagger-2-what-is-it-key-concepts-and-more/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.