paint-brush
使用 Dagger 2 进行依赖注入:它是什么、关键概念等等经过@dilip2882
412 讀數
412 讀數

使用 Dagger 2 进行依赖注入:它是什么、关键概念等等

经过 Dilip Patel21m2024/08/28
Read on Terminal Reader

太長; 讀書

依赖注入 (DI) 是一种用于实现控制反转 (IoC) 的设计模式。它是一种将创建对象的责任转移到代码其他部分的技术。这促进了松散耦合,使代码更加模块化且更易于管理。
featured image - 使用 Dagger 2 进行依赖注入:它是什么、关键概念等等
Dilip Patel HackerNoon profile picture
0-item


依赖注入简介

依赖注入 (DI) 是一种用于实现控制反转 (IoC) 的设计模式,其中创建和管理依赖项的控制从应用程序转移到外部实体。这有助于创建更模块化、可测试和可维护的代码。这是一种将创建对象的责任转移到代码其他部分的技术。这促进了松散耦合,使代码更加模块化且更易于管理。

类通常需要引用其他类才能正常运行。例如,假设Library类需要Book类。这些必需的类称为依赖项。Library Library依赖于Book类的实例才能运行。

hyperskill.org

类获取其所需对象的主要方式有三种:

  1. 自我构造:类创建并初始化自己的依赖项。例如, Library类将创建并初始化自己的Book类实例。
  2. 外部检索:该类从外部源检索依赖项。一些 Android API(例如Context getters 和getSystemService()以这种方式工作。
  3. 依赖注入:在构造类时或通过需要依赖项的方法为类提供依赖项。例如, Library构造函数将接收Book实例作为参数。

第三个选项是依赖注入!使用 DI,您可以提供类的依赖项,而不是让类实例本身获取它们。

没有依赖注入的示例

如果没有 DI,创建自己的Book依赖项的Library可能如下所示:

 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(); } }

这不是 DI 的示例,因为Library类构造了自己的Book 。这可能会有问题,因为:

  • 紧密耦合LibraryBook紧密耦合。Library Library一个实例使用一种Book类型,因此很难使用子类或其他实现。
  • 测试难度:对Book的严格依赖使测试更具挑战性。Library Library Book的真实实例,从而阻止使用测试替身来修改Book以适应不同的测试用例。

依赖注入示例

使用 DI, Library的每个实例不再构造自己的Book对象,而是在其构造函数中接收Book对象作为参数:

 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(); }

main 函数使用Library 。由于Library依赖于Book ,因此应用程序会创建Book的一个实例,然后使用它来构造Library的一个实例。这种基于 DI 的方法有以下好处:

  • Library的可重用性:您可以将Book的不同实现传递给Library 。例如,您可以定义一个名为EBookBook新子类,并希望Library使用。使用 DI,您只需将EBook的一个实例传递给Library ,它就可以正常工作,无需进一步更改。
  • 轻松测试Library :您可以传递测试替身来测试不同的场景。

另一个 DI 示例

考虑这样一种情况,即NotificationService类依赖于Notification类。如果没有 DI, NotificationService会直接创建Notification的实例,这使得使用不同类型的通知或使用各种通知实现测试服务变得很困难。

为了说明 DI,让我们重构这个例子:

 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(); } }

现在, NotificationService依赖于Notification接口,而不是特定的类。这允许可以互换使用不同的Notification实现。您可以通过sendNotification方法设置要使用的实现:

 NotificationService service = new NotificationService(); service.sendNotification(new EmailNotification()); service.sendNotification(new SMSNotification());

Android 中的依赖注入方法

DI 主要有三种类型:

  1. 方法(接口)注入:依赖项通过方法传递,类可以通过接口或其他类访问这些方法。上例演示了方法注入。
  2. 构造函数注入:依赖项通过其构造函数传递给类。
 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. 字段注入(或 Setter 注入) :某些 Android 框架类(例如 Activity 和 Fragment)由系统实例化,因此无法进行构造函数注入。使用字段注入,依赖项在类创建后实例化。

 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. 方法注入:通过方法提供依赖项,通常使用@Inject注解。

依赖注入的优点

  • 类的可重用性增强,对特定实现的依赖性降低。这是由于控制反转,类不再管理其依赖项,而是使用提供的任何配置。
  • 依赖项是 API 表面的一部分,可以在对象创建或编译时进行验证,从而使重构更容易。
  • 由于类不管理其依赖项,因此可以在测试期间传入不同的实现以覆盖各种场景。

自动依赖注入

在上一个示例中,您手动创建、提供和管理了不同类的依赖项,而无需使用库。这种方法称为手动依赖项注入。虽然它适用于简单的情况,但随着依赖项和类的数量增加,它会变得繁琐。手动依赖项注入有几个缺点:

  • 样板代码:对于大型应用程序,管理所有依赖项并正确连接它们可能会导致大量重复代码。在多层架构中,为顶层创建对象需要为其下方的层提供所有依赖项。例如,要构建计算机,您需要 CPU、主板、RAM 和其他组件;而 CPU 可能需要晶体管和电容器。
  • 复杂的依赖管理:当您无法预先构建依赖项(例如使用延迟初始化或将对象范围限定到应用程序中的特定流)时,您需要编写和维护自定义容器(或依赖图)来管理内存中依赖项的生命周期。

库可以通过为您创建和提供依赖项来自动化此过程。这些库分为两类:

  1. 基于反射的解决方案:这些解决方案在运行时连接依赖项。
  2. 静态解决方案:这些解决方案在编译时生成连接依赖项的代码。

Dagger 是一个流行的 Java、Kotlin 和 Android 依赖注入库,由 Google 维护。Dagger 通过为您创建和管理依赖关系图来简化应用中的 DI。它提供完全静态的编译时依赖关系,解决了与基于反射的解决方案(如 Guice)相关的许多开发和性能问题。

基于反射的解决方案

这些框架在运行时连接依赖项:

  1. Toothpick :使用反射连接依赖项的运行时 DI 框架。它设计为轻量级且快速,适合 Android 应用程序。

静态解决方案

这些框架在编译时生成连接依赖项的代码:

  1. Hilt :Hilt 建立在 Dagger 之上,提供了一种将 Dagger 依赖注入纳入 Android 应用程序的标准方法。它通过提供预定义的组件和范围简化了 Dagger 的设置和使用
  2. Koin :一个轻量级且简单的 Kotlin DI 框架。Koin 使用 DSL 来定义依赖项,并且易于设置和使用。
  3. Kodein :基于 Kotlin 的 DI 框架,易于使用和理解。它提供了一个简单灵活的 API 来管理依赖项。

依赖注入的替代方案

依赖注入的替代方案是服务定位器模式。此设计模式还有助于将类与其具体依赖项分离。您可以创建一个称为服务定位器的类,该类可创建和存储依赖项,并根据需要提供它们。

 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() }

服务定位器模式与依赖注入的区别在于依赖项的使用方式。使用服务定位器模式时,类会请求所需的依赖项;使用依赖注入时,应用会主动提供所需的对象。

什么是 Dagger 2?

Dagger 2 是一款流行的 Android DI 框架。它使用编译时代码生成,并以其高性能而闻名。Dagger 2 通过生成处理依赖项所需的代码来简化依赖项注入过程,从而减少样板并提高效率。

Dagger 2 是一个基于注释的 Android 依赖注入库。以下是关键注释及其用途:

  • @Module :用于定义提供依赖项的类。例如,模块可以为 Retrofit 提供ApiClient
  • @Provides :注释模块中的方法来指定如何创建和返回依赖项。
  • @Inject :用于请求依赖项。可应用于字段、构造函数和方法。
  • @Component :连接@Module@Inject的接口。它包含所有模块并为应用程序提供构建器。
  • @Singleton :确保创建依赖项的单个实例。
  • @Binds :用于抽象类中提供依赖关系,类似于@Provides ,但更简洁。

Dagger 组件

Dagger 可以为您的项目生成依赖关系图,从而让它在需要时确定从哪里获取依赖项。要实现此功能,您需要创建一个接口并使用@Component对其进行注释。

@Component接口中,您可以定义返回所需类实例的方法(例如BookRepository )。 @Component注释指示 Dagger 生成一个容器,其中包含满足其公开类型所需的所有依赖项。此容器称为 Dagger 组件,它包含 Dagger 知道如何提供的对象图及其依赖项。


例子

让我们考虑一个涉及LibraryRepository的示例:

  1. 注释构造函数:向LibraryRepository构造函数添加@Inject注释,以便 Dagger 知道如何创建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. 注释依赖项:类似地,注释依赖项的构造函数( LocalLibraryDataSourceRemoteLibraryDataSource ),以便 Dagger 知道如何创建它们。

 public class LocalLibraryDataSource { @Inject public LocalLibraryDataSource() { // Initialization code } } public class RemoteLibraryDataSource { private final LibraryService libraryService; @Inject public RemoteLibraryDataSource(LibraryService libraryService) { this.libraryService = libraryService; } }

3. 定义组件:创建一个用@Component注释的接口来定义依赖图。

 @Component public interface ApplicationComponent { LibraryRepository getLibraryRepository(); }

当你构建项目时,Dagger 会为你生成ApplicationComponent接口的实现,通常名为DaggerApplicationComponent

用法

您现在可以使用生成的组件来获取类的实例并自动注入它们的依赖项:

 public class MainApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.create(); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }

在您的活动或片段中,您可以检索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 } }

Dagger 2 中的关键概念

1. 模块
∘ 模块的关键概念
∘ 在组件中包含模块
2. 范围
3. 组件
4. 组件依赖关系
5.运行时绑定

1. 模块

Dagger 2 中的模块是使用@Module注释的类,用于为组件提供依赖项。它们包含使用@Provides@Binds注释的方法,用于指定如何创建和提供依赖项。模块对于组织和管理应用程序所需的对象的创建至关重要。

模块的关键概念

  1. @Module 注解:此注解用于将类定义为 Dagger 模块。模块类包含提供依赖项的方法。
  2. @Provides 注解:此注解用于模块内的方法,表示该方法提供某种依赖关系。这些方法负责创建和返回依赖关系的实例。
  3. @Binds 注解:此注解用于抽象类中,将实现绑定到接口。它比@Provides更简洁,适用于模块为抽象类的情况。

模块示例

@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(); } }

在这个例子中, NetworkModule是一个用@Module注释的类。它包含两个用@Provides注释的方法,用于创建并返回RetrofitOkHttpClient的实例。

使用 @Binds

当你有一个接口及其实现时,可以使用@Binds将实现绑定到接口。这比使用@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); }

在这个例子中, ApiModule是一个用@Module注解的抽象类。 bindApiService方法用@Binds注解,用于将ApiServiceImpl绑定到ApiService

模块可以根据其提供的功能进行组织。例如,您可以为网络操作、数据库操作和 UI 相关依赖项设置单独的模块。

例子:

  • NetworkModule :提供与网络相关的依赖项,例如RetrofitOkHttpClient
  • DatabaseModule :提供与数据库相关的依赖项,例如RoomDatabase
  • UIModule :提供与 UI 相关的依赖项,例如ViewModelPresenter

在组件中包含模块

模块包含在组件中,用于为需要它们的类提供依赖项。设置方法如下:

应用程序组件.java:

 @Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }

在这个例子中, ApplicationComponent包括NetworkModuleDatabaseModule来为应用程序提供依赖项。

2. 范围

Dagger 2 中的范围是定义依赖项生命周期的注释。它们确保在指定范围内创建和共享依赖项的单个实例。这有助于有效管理内存并确保在适当的情况下重用依赖项。

  • 单例范围:确保在整个应用程序的生命周期中依赖项只有一个实例。
  • 活动范围:确保活动生命周期内依赖关系的单个实例。
  • 片段范围:确保片段生命周期内依赖项的单个实例。

1. 单例范围

定义@Singleton范围确保在整个应用程序的生命周期内创建并共享依赖项的单个实例。

此范围通常用于需要在整个应用程序内共享的依赖项,例如网络客户端、数据库实例或共享首选项。

例子:

 @Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }

在这个例子中, @Singleton注解确保NetworkModuleDatabaseModule提供的RetrofitDatabase实例是单例,并在整个应用程序中共享。

2. 活动范围

定义@ActivityScope (自定义范围)确保在活动的生命周期内创建并共享依赖项的单个实例。

此范围对于特定于活动的依赖项很有用,并且每次重新创建活动时都应重新创建,例如演示者或视图模型。

例子

 @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope { } @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }

在此示例中, @ActivityScope注释确保ActivityModule提供的依赖项作用域于活动的生命周期。

3. 片段范围

定义@FragmentScope (另一个自定义范围)确保在片段的生命周期内创建并共享依赖项的单个实例。

用例:此范围对于特定于片段的依赖项很有用,并且每次重新创建片段时都应重新创建,例如特定于片段的演示者或视图模型。

例子

 @Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScope { } @FragmentScope @Component(dependencies = ActivityComponent.class, modules = FragmentModule.class) public interface FragmentComponent { void inject(MyFragment myFragment); }

在此示例中, @FragmentScope注释确保FragmentModule提供的依赖项作用域于片段的生命周期。

3. 组件

组件依赖关系允许一个组件依赖于另一个组件,从而实现依赖关系的重用。组件依赖关系主要有两种类型:

  • 应用程序组件:提供整个应用程序所需的依赖项。
  • 活动组件:提供特定活动内所需的依赖项。

1. 应用程序组件

定义:应用程序组件提供整个应用程序所需的依赖项。它通常使用@Singleton来确保依赖项在整个应用程序中共享。

该组件用于需要全局可用的依赖项,例如网络客户端、数据库实例或共享首选项。

例子

 @Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }

在这个例子中, ApplicationComponent负责提供RetrofitDatabase实例,它们在整个应用程序共享。

2. 活动组件

定义:活动组件提供特定活动所需的依赖项。它通常使用自定义范围(例如@ActivityScope )来限定范围,以确保每次重新创建活动时都会重新创建依赖项。

该组件用于特定于活动的依赖项,例如演示者或视图模型。

例子

 @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }

在此示例中, ActivityComponent依赖于ApplicationComponent并提供特定于MainActivity的依赖项。

4. 组件依赖关系

组件依赖关系允许一个组件依赖于另一个组件,从而实现依赖关系的重用。组件依赖关系主要有两种类型:

  • 子组件:子组件是另一个组件的子组件,可以访问其父组件的依赖项。
  • 依赖属性:这允许一个组件依赖于另一个组件,而无需成为子组件。

1.子组件:

子组件是另一个组件的子项,可以访问其父组件的依赖项。子组件在父组件内定义,可以继承其范围。

例子

 @ActivityScope @Subcomponent(modules = ActivityModule.class) public interface ActivitySubcomponent { void inject(MainActivity mainActivity); }

在这个例子中, ActivitySubcomponent是父组件的子组件,并可以访问其依赖项。

2. 依赖属性

这允许组件依赖另一个组件,而无需成为子组件。依赖组件可以访问父组件提供的依赖项。

例子

 @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }

在这个例子中, ActivityComponent依赖于ApplicationComponent并且可以访问其依赖项。

5. 运行时绑定

Dagger 2 中的运行时绑定是指根据需要的上下文在运行时创建和管理的依赖项的提供。

  • 应用程序上下文:用于需要与应用程序一起存在的依赖项。
  • 活动上下文:用于需要与活动一样长久存在的依赖项。

1. 应用背景

定义:应用程序上下文是与整个应用程序的生命周期相关的上下文。它用于需要与应用程序本身一样长的依赖项。

在整个应用程序中共享的依赖项,无需为每个活动或片段重新创建。示例包括网络客户端、数据库实例和共享首选项。

例子

 @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(); } }

在此示例中, AppModule将应用程序上下文作为单例依赖项提供。 provideApplicationContext方法确保提供的上下文与应用程序的生命周期相关联。

2. 活动背景

定义:活动上下文是与特定活动生命周期相关的上下文。它用于需要与活动本身同时存在的依赖项。

特定于某项活动的依赖项,每次重新创建活动时都应重新创建。示例包括视图模型、演示器和与 UI 相关的依赖项。

例子

 @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; } }

在此示例中, ActivityModule将活动上下文作为范围依赖项提供。 provideActivityContext方法确保提供的上下文与活动的生命周期相关联。

在组件中使用运行时绑定

要使用这些运行时绑定,您需要在组件中包含相应的模块:

应用程序组件

 @Singleton @Component(modules = {AppModule.class, NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); Context getApplicationContext(); }

活动组件

 @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); Context getActivityContext(); }

注入上下文

设置好组件和模块后,您可以根据需要将上下文注入到类中。

活动中的示例

 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); } }

在此示例中, MainActivity通过依赖项注入接收活动上下文和应用程序上下文。这允许活动根据依赖项的特定需求使用适当的上下文。

示例:在 Android 应用程序中使用 Dagger 2

设置 Dagger 2

要在您的项目中使用 Dagger 2,您需要在build.gradle文件中添加以下依赖项:

 dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }

2.x替换为最新版本的 Dagger 2。

步骤 1:定义模块

创建一个模块来提供依赖项。例如,一个NetworkModule来提供Retrofit实例:

 @Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } }

步骤 2:定义组件

创建一个组件来连接模块和需要依赖项的类:

 @Singleton @Component(modules = {NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }

步骤 3:注入依赖项

使用组件将依赖项注入到您的类中。例如,在您的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; } }

步骤 4:使用注入依赖项

现在,您可以在类中使用注入的依赖项。例如,在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 // ... } }

结论

我们来总结一下这个话题:

  • DI 的主要目的是松开耦合,使得管理依赖关系变得更容易。
  • 通过使用 DI,您可以增加代码灵活性并简化测试过程。
  • DI 是一个复杂的主题,根据场景有不同的实现。
  • 不同语言的 DI 具有各自的特性,这会影响您的使用方式。
  • Dagger 2 自动化了创建和提供依赖项的过程,减少了样板代码并提高了可维护性。
  • Dagger 2 提供编译时安全性,确保应用程序运行前所有依赖项都得到满足。
  • 通过在编译时生成代码,Dagger 2 避免了基于反射的解决方案相关的性能开销。