6

Dagger之十三、好刀配好鞘 — Hilt

 2 years ago
source link: http://blog.chengyunfeng.com/?p=1125
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Dagger之十三、好刀配好鞘 — Hilt

作者: rain 分类: Android Training 发布时间: 2021-10-30 13:19 6 0条评论

不可否认,虽然通过 Dagger 安卓扩展功能使用 Dagger 看起来不用重复写那些通用的代码了,但是其使用起来一点都不方便,使用门槛还是比较高。安卓开发团队一直致力于解决在安卓项目中使用 Dagger 的复杂情况,想把 Dagger 的使用门槛降低,让广大开发者都可以使用 Dagger。经过一年多的研发,安卓开发团队推出了 Hilt 框架,进一步简化 Dagger 的使用。

Hilt 提供了一种标准的方式来在安卓应用中使用 Dagger。 Hilt 的设计目标如下:

  • 简化在安卓应用中使用 Dagger 相关的代码
  • 定义了一些易于使用的、标准的部件和范围(components and scopes),这些定义比较容易理解和在应用间共享代码。
  • 一种更简单的提供依赖对象的方式,并且可以很方便的为不同的构建类型(build types)提供不同的依赖对象,比如为 testing、Debug 或者 release 类型提供不同的对象。

下面先来看看如何使用 Hilt,感受下 Hilt 所提供的能力,以及如何简化了在安卓应用中使用 Dagger 所需要编写的代码。

在项目中添加 Hilt 依赖项

Hilt 是通过 Gradle 插件来动态生成之前在安卓应用开发中需要手工编写的代码的,所以需要先使用 hilt 插件。在您的项目的根目录的 build.gradle 文件中添加 hilt-android-gradle-plugin 插件:

buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'

然后在您的 app/build.gradle 文件中启用该 Gradle 插件:

apply plugin: 'com.android.application'
apply plugin: 'dagger.hilt.android.plugin'
android {
dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    annotationProcessor 'com.google.dagger:hilt-android-compiler:2.28-alpha'

如果您在项目中使用了 Kotlin 则需要使用 kapt:

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"

如果您同时使用了 Data binding 那么需要使用 4.0 以上版本的 Android Studio。

Hilt 使用了 Java 8 里面的新功能,所以需要在 app/build.gradle 中同时启用 Java 8:

android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8

使用 Hilt Application 类

所有使用 Hilt 的应用都需要包含一个自定义的 Application 类,并且在该类上使用 @HiltAndroidApp 注解。

@HiltAndroidApp 触发 Hilt 来生成需要的代码,是 Hilt 生成代码的入口。并且该自定义的 Application 类也作为全局应用级别部件(Dagger 中的 ApplicationComponent)的容器。

@HiltAndroidApp
public class MyApplication extends Application {

这样 Hilt 就会为该义 Application 生成一个 Dagger 部件(和我们之前是示例项目中定义的 ApplicationComponent 一样,现在是 Hilt 自动生成该部件了),并且该部件和 Application 的生命周期绑定,并且提供需要的依赖对象。
这个自动生成的 Application 部件就是该应用的 父部件,其他所有的子部件都可以依赖该部件中所提供的对象。

在安卓组件中注入需要的对象

Application 上设置好 Hilt 后,就可以开始使用这个 Application 级别的父部件了,在需要注入对象的其他安卓组件中使用 @AndroidEntryPoint 注解就可以实现自动注入了:

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject
    MainViewModel mMainViewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 这里不需要在手工调用 Dagger 部件来注入该 Activity 了

比如上面示例中,只需要在 MainActivity 上使用 @AndroidEntryPoint,然后在该类中需要注入的变量上使用 @Inject 注解就完工了,不需要在 onCreate() 函数中从 Application 中获取到子部件,然后再调用 inject(Activity a) 函数了。

目前 Hilt 支持在如下的安卓组件类上使用 @AndroidEntryPoint 注解:
– Activity
– Fragment
– View
– Service
– BroadcastReceiver

对于自定义 Application 类中需要注入的变量,可以通过使用 @HiltAndroidApp 注解,而不是@AndroidEntryPoint

如果您在安卓类上使用了 @AndroidEntryPoint 注解,那么该类所依赖的其他安卓组件也需要使用 @AndroidEntryPoint 注解。例如,如果您在一个 Fragment 上使用了,那么在使用该 FragmentActivity 上也需要使用 @AndroidEntryPoint 注解(我们称这个 Fragment 依附于这 Activity,或者 这个 Activity 拥有这个 Fragment)。

使用 Hilt 的一些限制:
– Hilt 只支持继承自 ComponentActivity 的 Activity,比如 AppCompatActivity
– Hilt 只支持继承自 androidx.Fragment 的 Fragment。
– Hilt 不支持 retained fragments

对于每个使用 @AndroidEntryPoint 注解的安卓组件, Hilt 都为该类生成了一个 Dagger 子部件,这些子部件可以从父部件中获取依赖的对象。

如果使用 @AndroidEntryPoint 的类继承了其他的类,并且在继承的类中也使用了 @Inject 来注入需要的对象,那么如果所继承的父类是抽象类的话,在该抽象类上不需要使用 @AndroidEntryPoint

提供部件中所需要的依赖对象

提供依赖对象和之前 Dagger 中的方式一样,还是通过 Dagger Module(模块) 来提供的,区别就是需要在 Dagger 模块上使用 @InstallIn 注解指定该 模块 是为那个部件提供依赖对象的(也就是该模块是安装到那个部件上的)。

对于项目中的类,通过在构造函数上使用 @Inject 注解同样可以把该对象添加到 Dagger 部件中。而对于无法在构造函数上使用 @Inject 注解的类就需要使用 Dagger模块了。

@Module
@InstallIn(ApplicationComponent.class)
public class ContextModule {
    @Provides // 说明这是一个对象提供函数
    @IntoMap // 说明该函数提供的对象被放到一个 Map value 中
    @IntKey(Sensor.TYPE_STEP_COUNTER)
        // 定义 Map 的 key
    Sensor providerStepSensor(@ApplicationContext Context context) {
        SensorManager manager = context.getSystemService(SensorManager.class);
        return manager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
    @Provides
    @IntoMap
    @IntKey(Sensor.TYPE_ACCELEROMETER)
    Sensor providerAccSensor(@ApplicationContext Context context) {
        SensorManager manager = context.getSystemService(SensorManager.class);
        return manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

上面的 ContextModule 模块被安装到了 ApplicationComponent(下面会解释) 中 (通过 @InstallIn(ApplicationComponent.class) 注解),这样该模块中所提供的对象在整个应用中都可以使用。如果您把该模块安装到了 ActivityComponent 中,那么只有 Activity 可以使用里面的对象,而 Application 是无法访问的,和 Dagger 中的子部件和父部件的关系一样。

Hilt 中的模块和 Dagger 中的模块一样,唯一的区别就是需要通过 @InstallIn 来告诉 Hilt 该模块需要安装到那个部件中去。所以在模块中可以使用 @Binds 来绑定接口和实现类,还可以使用 @Qualifier 限定符以及绑定到集合等功能。

Hilt 预定义的限定符(@Qualifier)

为了方便使用, Hilt 预定了一些在安卓开发中常用的对象对应的限定符,例如当你需要一个 Context 对象的时候,可以通过 @ApplicationContext@ActivityContext 来指定需要的 Context 类型,如果需要一个 Application 类型的则可以通过使用 @ApplicationContext 限定符来实现。

在上面的Dagger 模块中我们就使用了该限定符:

    @Provides
    @IntoMap
    @IntKey(Sensor.TYPE_ACCELEROMETER)
    Sensor providerAccSensor(@ApplicationContext Context context) {
        SensorManager manager = context.getSystemService(SensorManager.class);
        return manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

Hilt 为安卓类所生成的部件

对于每个使用 @AndroidEntryPoint 的安卓组件,Hilt 都为其自动生成了一个 Dagger 子部件,Dagger 模块上的 @InstallIn 注解就需要设置一个对应的子部件,代表该模块里面的对象是安装到那个部件中的。 每个部件负责注入安卓模块中使用了 @Inject 的变量。

下面是 Hilt 所提供的一些 部件以及该部件所对应的安卓组件:

Hilt所生成的子部件 用于注入的安卓组件 ApplicationComponent Application ActivityRetainedComponent ViewModel ActivityComponent Activity FragmentComponent Fragment ViewComponent View ViewWithFragmentComponent View annotated with @WithFragmentBindings ServiceComponent Service

注意上面并没有 BroadcastReceiverContentProvider,对于 BroadcastReceiver Hilt 直接从 ApplicationComponent 中注入,而 ContentProvider 并不常用,需要我们自己来使用自定义入口功能,后面会介绍。

部件的生命周期

Hilt 自动管理所生成的 Dagger 部件以及里面的对象,Hilt 根据安卓组件的生命周期来创建部件和销毁部件:

Hilt 生成的部件 部件被创建的时刻 部件被销毁的时刻 ApplicationComponent Application#onCreate() Application#onDestroy() ActivityRetainedComponent Activity#onCreate() Activity#onDestroy() ActivityComponent Activity#onCreate() Activity#onDestroy() FragmentComponent Fragment#onAttach() Fragment#onDestroy() ViewComponent View#super() View destroyed ViewWithFragmentComponent View#super() View destroyed ServiceComponent Service#onCreate() Service#onDestroy()

注意 ActivityRetainedComponent 可以跨越 Activity 的配置改变事件(比如横屏切换到竖屏的情况),所以该部件是在第一次调用 Activity#onCreate() 的时候被创建的,在最后一次调用 Activity#onDestroy() 的时候被销毁的。

部件的范围(Scope)

Hilt 和 Dagger一样默认里面所有的对象都是无范围的,每次注入该对象都会重新生成一个或者重新调用一次对象提供函数。

Hilt 预定义了一些自定义范围注解,使用这些注解可以把依赖的对象绑定到对应的部件中,这样在该部件中只有一个该对象实例存在。 下面是每个部件所对应的范围注解:

安卓组件类 Hilt 生成的部件 对应的范围注解 Application ApplicationComponent @Singleton View Model ActivityRetainedComponent @ActivityRetainedScope Activity ActivityComponent @ActivityScoped Fragment FragmentComponent @FragmentScoped View ViewComponent @ViewScoped View annotated with @WithFragmentBindings ViewWithFragmentComponent @ViewScoped Service ServiceComponent @ServiceScoped

比如对于项目中的 OkHttpClient 客户端我们期望其为单例的,在整个应用中只有一个实例,那么就可以在 Dagger 模块的对象提供函数上使用 @Singleton:

@Module
@InstallIn(ApplicationComponent.class)
public class NetworkModule {
  @Singleton
  @Provides
  public static OkHttpClient provideOkHttpClient(
    AuthInterceptor authInterceptor
      return new OkHttpClient.Builder()
                   .addInterceptor(authInterceptor)
                   .build();

注意在 Dagger 中,模块对象的范围必须要和部件的范围一致,所以对于 @Singleton 类型的对象,只能安装到对应的 ApplicationComponent 中,所以上面的 NetworkModule 使用了注解 @InstallIn(ApplicationComponent.class)

Hilt 生成的部件的继承关系

对于每个部件中的对象,在该部件和子部件中都可以访问。下图是 Hilt 所生成的部件的继承关系:

https://developer.android.google.cn/images/training/dependency-injection/hilt-hierarchy.svg

在上面的继承关系中,ViewComponent 部件有点特别,默认情况下 ViewComponent 只能从父部件 ActivityComponent 中获取里面的对象,而不能从 FragmentComponent 中获取,如果您的 View 在 Fragment 中使用,希望使用 FragmentComponent 中的对象,那么可以在 View 上同时使用 @AndroidEntryPoint@WithFragmentBindings 注解,这样 Hilt 就会为该 View 生成 ViewWithFragmentComponent 子部件,这个子部件是 FragmentComponent 的子部件。

部件中默认的依赖对象

Hilt 在生成每个部件的时候, 默认在这些部件中添加了一些安卓应用中常用的系统对象。比如 Context 对象,这样在使用的时候,您可以直接使用这些 Hilt 所添加的系统对象。** 注意,这些默认的对象和具体的安卓组件没关系(比如说,某一个 Activity),Hilt 是使用同一个 ActivityComponent 来实现对应用中所有 Activity 的注入操作,只不过每个 Activity 使用的是不同的 ActivityComponent 实例。**

下面是每个部件中默认提供的安卓对象:

Hilt 生成的部件 默认的依赖对象 ApplicationComponent Application ActivityRetainedComponent Application ActivityComponent Application, Activity FragmentComponent Application, Activity, Fragment ViewComponent Application, Activity, View ViewWithFragmentComponent Application, Activity, Fragment, View ServiceComponent Application, Service

在安卓中,由于 Context 对象比较常用,并且 ApplicationActivity 都继承自 Context,所以对于 Context 对象的注入,可以通过 @ApplicationContext@ActivityContext 来说明是需要注入 Application 还是 Activity 对象,除了使用 限定符外,还可以直接使用 ApplicationFragmentActivity 这两个类型,比如:

public class AnalyticsAdapter {
  private final Context context;
  // 通过使用 @ActivityContext 限定注入 Activity 对象
  @Inject
  AnalyticsAdapter(@ActivityContext Context context) {
    this.context = context;
// 直接使用 FragmentActivity 类型, 不需要指定 @ActivityContext 限定符了
public class AnalyticsAdapter {
  private final FragmentActivity activity;
  @Inject
  AnalyticsAdapter(FragmentActivity activity) {
    this.activity = activity;

在 Hilt 默认不支持的类中使用对象注入

Hilt 对常见的安卓组件提供了支持,而对于一些不太常用的地方就需要我们自己来实现了。 这种情况就需要使用 @EntryPoint 注解来定义一个自定义的 Dagger 部件入口。每个 部件入口(entry point)是 Dagger(Hilt) 代码和我们应用中自己写的代码之间的桥梁,我们的代码通过该入口来使用 Dagger 里面所管理的对象。

前面我们说过, Hilt 默认不支持 ContentProvider ,如果想在 ContentProvider 里面使用对象注入,那么就需要使用 @EntryPoint 来自定义一个入口,在该入口中需要定义在 ContentProvider 中需要依赖的对象(可以使用 Qualifier 限定符),然后把该入口作为子部件安装到对应的 Hilt 所支持的部件中:

public class ExampleContentProvider extends ContentProvider {
  @EntryPoint
  @InstallIn(ApplicationComponent.class)
  interface ExampleContentProviderEntryPoint {
    public AnalyticsService analyticsService();

上面的 ExampleContentProvider 中使用 @EntryPoint 定义了一个入口,并且安装到了 ApplicationComponent 中,在该入口中通过函数 AnalyticsService analyticsService(); 说明 ContentProvider 中需要依赖 AnalyticsService 对象,Dagger 会从 ApplicationComponent 部件中获取该对象。

通过 EntryPointAccessors 的静态方法来获取所定义的入口对象实例,该函数需要的参数可能是部件实例或者拥有部件的 @AndroidEntryPoint 对象。用来获取自定义入口对象的静态函数的参数对象需要和 @InstallIn 中所指定的部件一致,如果该入口安装到了 ApplicationComponent 中,那么你就需要提供一个 ApplicationComponent 对象或者拥有 ApplicationComponentApplication 对象。

public class ExampleContentProvider extends ContentProvider {
  @Override
  public Cursor query(...) {
    Context appContext = getContext().getApplicationContext();
    ExampleContentProviderEntryPoint hiltEntryPoint =
      EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint.class);
    AnalyticsService analyticsService = hiltEntryPoint.analyticsService();

上面是 Hilt 相关的主要内容,通过和 Dagger 对比可以发现 Hilt 通过预定义一些标准的注解来简化在安卓开发中使用 Dagger。 通过 Hilt 你只需要做如下几件事即可,不再需要手工编写执行注入的代码了:

  • 通过在自定义的 Application 中使用 @HiltAndroidApp 告诉 Hilt 该项目需要使用 Dagger 来注入对象,
  • 通过在安卓组件上(比如 Activity)使用 @AndroidEntryPoint 注解告诉 Hilt ,该类里面有变量需要 Dagger 来注入,
  • 通过 @Module@InstallIn 注解告诉 Dagger 该模块中的对象需要安装到某个部件中,这样该部件就可以对外提供这些对象了

通过以上三步就可以使用 Dagger 来管理需要依赖的对象了。使用 Hilt 你只需关注需要 Dagger 管理的对象以及在安卓组件中需要使用这些对象,大大简化了在安卓开发中使用 Dagger 的方法。

由于 Hilt 通过标准化的方式来简化 Dagger 的使用,所以在使用方式的需要按照 Hilt 要求的来,并且使用的灵活性会变的差一些,在后面把我们的示例项目迁移到 Hilt 的时候我们会看到这种情况,但是通过一些变通方式,还是能够满足项目要求的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK