Jetpack

依赖注入库Hilt的使用和理解,一篇就够了

2020-10-16  本文已影响0人  kevinsEegets
timg (3).jpeg

[TOC]

1 Hilt相较于Dagger的优势

在我们了解Hilt之前,先需要知道Dagger, Dagger是Google提供的用于依赖注入的库,该库很多人可能都听过.Dagger的特点总结一下就是:牛逼,高端,难用.那这么牛逼高端的库为什么不用?答案就是后边两个字:难用!!!至于为什么难用,主要原因是该库是为了Java,Kotlin以及Android设计的,是为了解决基于反射解决方案引起的许多开发和性能问题,详细解释可以看看Dagger官网[https://dagger.dev/]以及我之前写的一篇简单文章[Dagger简单使用及实现原理(https://www.jianshu.com/p/9d80a6cb59f2])]

因为太难用了,所以Google专门为我们提供了一个使用更简单的依赖注入库Hilt.

Hilt是Android的依赖注入库,可减少在项目中执行手动DI[依赖注入]的样板代码,执行手动依赖注入需要手动构造每个类及依赖项,通过组件管理依赖项.

2 添加依赖项

首先将hilt-android-gradle-plugin插件添加到根项目的build.gradle文件中:

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

然后在app/build.gradle文件中添加依赖项:

...
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"
}

这里有一个需要注意的地方:同时使用 Hilt 和数据绑定的项目需要 Android Studio 4.0 或更高版本,最好是>=4.1版本,新版本支持依赖注入指向。

Hilt使用Java8功能,如需在项目中启动Java8,请在app/build.gradle文件中增加如下代码:

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

3 Hilt增加应用类注解

所有使用Hilt的项目都需要包含一个带有@HiltAndroidApp注释的Application类.

@HiltAndroidApp会触发Hilt代码生成,生成的代码包含应用的一个基类,该基类充当应用级依赖项容器。

3_1 Hilt为何要增加@HiltAndroidApp注解

首先我们进入@HiltAndroidApp`看看Google的注释

package dagger.hilt.android;
...

/*
 * <pre><code>
 *   {@literal @}HiltAndroidApp(Application.class)
 *   public final class FooApplication extends Hilt_FooApplication {
 *     {@literal @}Inject Foo foo;
 *
 *     {@literal @}Override
 *     public void onCreate() {
 *       super.onCreate();  // The foo field is injected in super.onCreate()
 *     }
 *   }
 * </code></pre>
 *
 * @see AndroidEntryPoint
 */
// Set the retention to RUNTIME because we check it via reflection in the HiltAndroidRule.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@GeneratesRootInput
public @interface HiltAndroidApp {
}

可以看到Hilt给我们的FooApplication生成了一个Hilt_FooApplication类,这个Hilt_FooApplication是做什么的我们稍后看看,另外我们可以看到在onCreate()我们可以增加依赖注入

我们在看看Hilt_FooApplication做了什么

public abstract class Hilt_TestHiltApplication extends Application implements GeneratedComponentManager<Object> {
  private final ApplicationComponentManager componentManager = new ApplicationComponentManager(new ComponentSupplier() {
    @Override
    public Object get() {
      return DaggerTestHiltApplication_HiltComponents_ApplicationC.builder()
          .applicationContextModule(new ApplicationContextModule(Hilt_TestHiltApplication.this))
          .build();
    }
  });
  ...
  @CallSuper
  @Override
  public void onCreate() {
    // This is a known unsafe cast, but is safe in the only correct use case:
    // TestHiltApplication extends Hilt_TestHiltApplication
    ((TestHiltApplication_GeneratedInjector) generatedComponent()).injectTestHiltApplication(UnsafeCasts.<TestHiltApplication>unsafeCast(this));
    super.onCreate();
  }
}
以查看【ApplicationContextModule】

如上代码是Hilt自动生成的继承自Application的类,该类大致做了两部分操作

总结一下:Application为什么要增加注解@HiltAndroidApp

4 依赖项注入Android类

在经历了步骤3操作且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint 注释的其他 Android 类提供依赖项:

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

Hilt 目前支持以下 Android 类:

有两点需要注意:

我们看看通过@AndroidEntryPoint注解了Activity之后Hilt帮我们做了什么?

@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ActivityGenerator")
public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManager<Object> {
  private volatile ActivityComponentManager componentManager;

  private final Object componentManagerLock = new Object();

  Hilt_MainActivity() {
    super();
  }

  Hilt_MainActivity(int contentLayoutId) {
    super(contentLayoutId);
  }

  @CallSuper
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    inject();
    super.onCreate(savedInstanceState);
  }

  @Override
  public final Object generatedComponent() {
    return componentManager().generatedComponent();
  }

  protected ActivityComponentManager createComponentManager() {
    return new ActivityComponentManager(this);
  }

  protected final ActivityComponentManager componentManager() {
    if (componentManager == null) {
      synchronized (componentManagerLock) {
        if (componentManager == null) {
          componentManager = createComponentManager();
        }
      }
    }
    return componentManager;
  }

  protected void inject() {
    ((MainActivity_GeneratedInjector) generatedComponent()).injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
  }

  @Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    ViewModelProvider.Factory factory = DefaultViewModelFactories.getActivityFactory(this);
    if (factory != null) {
      return factory;
    }
    return super.getDefaultViewModelProviderFactory();
  }
}

可以看出来:Hilt自动帮我们生成了一个继承自AppCompatActivity的名称为Hilt_MainActivity.java类,这个类里边代码不是很多,我们大概总结一下这段代码帮我们做了那些事?

上述代码有一个注意的地方,代码如下:

private volatile ActivityComponentManager componentManager;
...
protected final ActivityComponentManager componentManager() {
    if (componentManager == null) {
      synchronized (componentManagerLock) {
        if (componentManager == null) {
          componentManager = createComponentManager();
        }
      }
    }
    return componentManager;
  }

上述代码其实是一个双重检查锁,用于检测对象是否初始化,不清楚Java双重检查锁的可以参考Java中的双重检查锁(double checked locking)ViewModelProvider.Factory

5 增加“@Inject”注入

为了执行字段注入,在Dagger中我们通过@Inject实现依赖项绑定,Hilt中也是同理。

Hilt提供绑定信息的一种方式是构造函数注入。实现如下:

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

带有注释的构造函数的参数即是该类的依赖项。

在上述代码中,AnalyticsServiceAnalyticsAdapter的一个依赖项。因此,Hilt还必须提供AnalyticsService的实例。说白了就是:我们现在只是通过@Inject实现了依赖项的注入,但是我们并没有提供AnalyticsService实例,所以我们还需要定义Module,通过@Provides将我们的依赖项提供出去,要不然Hilt会无情的报错。如何提供请看【<u>6_2 @Provides注入实例</u>】

5_1 使用@ViewModelInject注入

@Inject一样,Hilt给我们提供了带有注释@ViewModelInect构造函数的依赖项,用于View用于注入的构造函数。

该ViewModel用于创建由androidx.hilt.lifecycle.HiltViewModelFactory并且可以在默认情况下检索Activity或Fragment带注解@AndroidEntryPoint,如下代码:

public class DonutViewModel {
      @ViewModelInject
      public DonutViewModel(@Assisted SavedStateHandle handle, RecipeRepository repository) {
          // ...
      }
  }
@AndroidEntryPoint
class StatisticsFragment : Fragment() {
    private val viewModel by viewModels<StatisticsViewModel>()
    ...
}

如上两处代码我们就实现了将View中的依赖项绑定到了StatisticsFragment中了。

之前我们在【<u>4 依赖项注入Android类</u>】有一个问题,为什么Hilt提供的Activity或Fragment为什么要提供ViewModelProvider.Factory这个工厂类?我们这就看看

我们翻看Hilt提供的Activity或Fragment部分源码可以看到如下:

@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ActivityGenerator")
public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManager<Object> {
  ...
  @Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    ViewModelProvider.Factory factory = DefaultViewModelFactories.getActivityFactory(this);
    if (factory != null) {
      return factory;
    }
    return super.getDefaultViewModelProviderFactory();
  }
}

可以看到,Hilt_MainActivity中的getDefaultViewModelProviderFactory(),其实是重写了Activity源码中的ViewModelProvider.Factory getDefaultViewModelProviderFactory(),目的是为了给我们提供一个DefaultViewModelProviderFactory,而DefaultViewModelProviderFactory恰好是我们viewModels绑定需要的,FragmentViewModelLazy.kt部分源码如下:

@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
...
@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

到这里我们就彻底明白了Hilt为我们生成的Hilt_XXXActivityHilt_XXXFragment中的全部代码了!

抛出一个问题,我们想在加注入的构造方法中增加默认值,该怎么做?,答案就是<u>5_1</u>以及<u>5_2</u>

5_2 使用构造方法增加默认参数

从之前所学可知,Hilt提供绑定信息的一种方式是构造函数注入。实现如下:

class AnalyticsAdapter @Inject constructor(
  private val title: String,
  private val des: String  
) { ... }

我们现在想给titledesc增加默认值该怎么办?我们在上述代码基础上修改一下,代码如下:

data class AnalyticsAdapter constructor(var title: String, val des: String) {
   @Inject
   constructor() : this("标题", "描述")
}

至于为什么不直接在构造函数中增加默认值,大概的原因还不支持在绑定的参数上直接赋值的操作,如果实在想支持可以使用<u>5_3 使用预定义限定符操作增加构造参数</u>方式来实现,我们的做法就是如上所示,再创建一个空的构造函数,在调用时Hilt会先调用我们内部的空构造函数,然后调用外部的构造并将值赋给参数。

5_3 限定符操作,为同一类型提供多个绑定

在<u>5_2 使用构造方法增加默认参数</u>当我们需要给定默认值时,我们需要重写构造,是不是比较麻烦,那有没有更简单的方式呢?答案是必须有,我们可以使用@Named</u>或@Qualifier实现多绑定,继续往下看:

5_3_1 使用@Named实现多个绑定
 @Module
 @InstallIn(ActivityComponent::class)
 object AnalyticsModule {
    @Named(NAME_TITLE)
    @Provides
    fun testNamed(): String {
        return "原始Named限定符标题"
    }

    @Named(NAME_DESC)
    @Provides
    fun test1Named(): String {
        return "原始Named限定符描述"
    }
 }
const val NAME_TITLE = "NameTitle"
const val NAME_DESC = "NameDesc"

// 第一种 Name方式
data class FakeService @Inject constructor(@Named(NAME_TITLE) var title: String, @Named(NAME_DESC) val des: String)

如上代码:我们首先有一个module提供类,我们通过@Providers提供了两个testNamed()test1Named(), 是我们需要注入的地方我们通过@Inject constructor(@Named(NAME_TITLE) var title: String, @Named(NAME_DESC) val des: String)获取我们提供的默认值。

同样的道理,我们用@Qualifier实现

5_3_2 使用@Qualifier实现多个绑定
@Module
 @InstallIn(ActivityComponent::class)
 object AnalyticsModule {

    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class QualifierTitle

    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class QualifierDesc

     @QualifierTitle
     @Provides
     fun testQualifier(): String {
         return "原始Qualifier标题"
     }

     @QualifierDesc
     @Provides
     fun test1Qualifier(): String {
         return "原始Qualifier描述"
     }
 }
data class Fake1Service @Inject constructor(@AnalyticsModule.QualifierTitle var title: String, @AnalyticsModule.QualifierDesc var des: String)

1:首先我们通过@Qualifier提供了两个注解类QualifierTitleQualifierDesc

2:我们通过@QualifierTitle@QualifierDesc对提供的方法进行限定

3:在注入的地方进行限定操作‘@Inject constructor(@AnalyticsModule.QualifierTitle var title: String, @AnalyticsModule.QualifierDesc var des: String)’

6 创建Hilt模块Module

6_1 @Bind注入接口实例

@Binds注释会告知Hilt在需要提供接口的实例时要使用哪种实现。

带有注释的函数会向Hilt提供以下信息:

我们用官方例子了解一下

//第一步:提供了一个接口`AnalyticsService`
interface AnalyticsService {
  fun analyticsMethods()
}
//第二步:`AnalyticsServiceImpl`实现`AnalyticsService`接口,并实现内部方法
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }
//第三步:可能还存在一个`AnalyticsServiceImpl_Test`也实现`AnalyticsService`接口,并实现内部方法
class AnalyticsServiceImpl_Test @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
  //第三步:通过`@Binds`将告知`AnalyticsService`的实现为`AnalyticsServiceImpl`而不是‘AnalyticsServiceImpl_Test’ 
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

6_2 @Provides注入实例

使用无法通过构造函数注入的情况,例如:RetrofitOkHttpClient,使用构建器模式创建实例,无法通过构造函数注入。那这个时候我们就可以用@Provides方式来提供依赖项。

带有注释的函数会向 Hilt 提供以下信息:

我们用一段代码来理解一下:

@Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
                .build()
    }

    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }

如上代码都不是通过构造函数实现注入。我们可以通过@Provides来实现对外部提供依赖项

7 Hilt中的预定义限定符

7_1 Google提供的预定义限定符

Android 组件 默认绑定
@ApplicationContext 获取应用上下文绑定
@ActivityContext 获取Activity上下文绑定

7_2 组件提供的限定符

Android 组件 默认绑定 注入器面向的对象
@ApplicationComponent Application Application
@ActivityRetainedComponent Application ViewModel
@ActivityComponent Application 和 Activity Activity
@FragmentComponent Application、Activity 和 Fragment Fragment
@ViewComponent Application、Activity 和 View View
@ViewWithFragmentComponent Application、Activity、Fragment 和 View 带有 @WithFragmentBindings 注释的 View
@ServiceComponent Application 和 Service Service

注意:ActivityRetainedComponent` 在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁。如下图:

[图片上传失败...(image-4018ab-1603855174131)]

8 Hilt提供的组件生命周期

8_1 组件生命周期

Hilt 会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。

生成的组件 创建时机 销毁时机
ApplicationComponent Application#onCreate() Application#onDestroy()
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ActivityComponent Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent View#super() 视图销毁时
ViewWithFragmentComponent View#super() 视图销毁时
ServiceComponent Service#onCreate() Service#onDestroy()

注意ActivityRetainedComponent 在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁。原因如上图

8_2 组件作用域

默认情况下,Hilt 中的所有绑定都未限定作用域。这意味着,每当应用请求绑定时,Hilt 都会创建所需类型的一个新实例。
下表列出了生成的每个组件的作用域注释:

Android 类 生成的组件 作用域
Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
带有 @WithFragmentBindings 注释的 View ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

8_3 组件层次结构

将模块安装到组件后,其绑定就可以用作该组件中的其他绑定依赖项,也可以用作组件层次结构中该组件下的任何子组件中的其他绑定依赖项:

[图片上传失败...(image-f7f169-1603855174131)]

注意:默认情况下,如果您在视图中执行字段注入,ViewComponent 可以使用 ActivityComponent 中定义的绑定。如果您还需要使用 FragmentComponent 中定义的绑定并且视图是 Fragment 的一部分,应将 @WithFragmentBindings 注释和 @AndroidEntryPoint 一起使用。

9 Hilt 对不支持的类如何执行字段注入?

Hilt支持常见的Android类。但是,您可能需要在Hilt不支持的类中执行字段注入。比如我们创建的一个普通的class,此时我们可用过@EntryPoint实现注入,如下:

@EntryPoint

class ExampleContentProvider : ContentProvider() {

  @EntryPoint
  @InstallIn(ApplicationComponent::class)
  interface ExampleContentProviderEntryPoint {
    fun analyticsService(): AnalyticsService
  }

  ...
}

如果需要访问ExampleContentProviderEntryPoint,可以使用EntryPointAccessors,代码如下:

class ExampleContentProvider: ContentProvider() {
    ...

  override fun query(...): Cursor {
    val appContext = context?.applicationContext ?: throw IllegalStateException()
    val hiltEntryPoint =
      EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)

    val analyticsService = hiltEntryPoint.analyticsService()
    ...
  }
}

注意:确保您以参数形式传递的组件和 EntryPointAccessors 静态方法都与 @EntryPoint 接口上的 @InstallIn 注释中的 Android 类匹配。

如上我们使用@InstallIn(ApplicationComponent::class),那么我们在获取时也要EntryPointAccessors.fromApplication(context?.applicationContext, XXXX::class.java), 保证context?.applicationContext,InstallIn提供的是一致的。

~~~over~~~

reference:https://developer.android.google.cn/training/dependency-injection/hilt-android#component-lifetimes

上一篇下一篇

猜你喜欢

热点阅读