Jetpack之Hilt讲解

993 阅读14分钟

JetPack之Hilt的使用

什么是依赖注入

    讲解Hilt之前,我们先来理解什么是依赖注入以及依赖注入有什么用。

    首先我们来看我们平时自己写代码

 class LearnHilt {
     var firstMember:FirstMember = ...
     var secondMember:SecondMember = ...
 }

    首先,这一个类有两个成员变量,firstMembersecondMember。这两个变量也叫做这个类的依赖。那么要初始化这两个依赖。无非就是两种方式。一种是类内部自己初始化。比如:

 class LearnHilt {
     var firstMember:FirstMember = FirstMember()
     var secondMember:SecondMember = SecondMember()
 }

    第二种就是让外部初始化,这种让外部初始化化内部依赖的方式,就叫做依赖注入

    所以回到一开始的问题,知道了什么是依赖注入了吗?就是让外部去初始化内部的依赖,这个就叫做依赖注入。那它有什么用呢?问这个问题之前,我们先想明白一个问题,我们有在使用依赖注入吗?比方说我们平时使用的有参构造函数

 data class User(var name:String)
 val user = User("zhangSan")

    这种方式到底是不是依赖注入呢?我们自己想一下,name是User类的一个依赖,那么初始化的依赖是他自己初始化的,还是外部提供的?"zhangsan"是外部提供给User的一个初始值,所以它也是依赖注入。所以大家明白了吗?不是依赖注入有什么用,依赖注入是一种帮助我们初始化依赖的方式,它在我们开发中无处不在。如果你还想不明白,你可以自己去多写一下依赖注入的代码,多体验一下。

    了解了什么是依赖注入,那么现在我们来聊聊Hilt,Hilt是谷歌官方提供给我们,帮助我们去更好的使用依赖注入

    接下来,我们就通过github.com/android/sun…这个例子去讲解Hilt的使用。首先是配置

首先是根目录下的配置

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

接着是app/build.gradle的配置

 apply plugin: 'kotlin-kapt'
 apply plugin: 'dagger.hilt.android.plugin'
 ​
 dependencies {
     implementation "com.google.dagger:hilt-android:2.38.1"
     kapt "com.google.dagger:hilt-android-compiler:2.38.1"
 }
 ​

其次还需要在app/build.gradle配置一下java8

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

这样一来,我们所有的配置都配置好了,接下来就可以去使用Hilt了。

Application的配置

    如果要在程序中使用Hilt,必须要在Application里面去首先配置。怎么配置,很简单。只需要一个注解。

 @HiltAndroidApp
 class MainApplication : Application(){}

    这样,Application关于Hilt的配资就完成了。那么接下来就是需要根据具体的业务去配置Hilt了。首先这里提示一下Hilt的注入功能只能从几个Android固定的入口点开始。为什么?因为Hilt是基于Dagger2去做了一些场景优化。Hilt一共支持6个android的入口点:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

    上面通过配置知道了Application是通过@HiltAndroidApp注解来声明注入的,其余的5个入口点均是通过@AndroidEntryPoint注解去注入的。例如Activity

 @AndroidEntryPoint
 class GardenActivity : AppCompatActivity() {
 ​
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView<ActivityGardenBinding>(this, R.layout.activity_garden)
     }
 }

那么怎么去依赖注入呢?首先在一个ViewModel中有一个依赖plantRepository

简单使用

 @AndroidEntryPoint
 class GalleryFragment : Fragment() {
     
     @Inject
     lateinit var viewModel: GalleryViewModel   //至于需要被依赖注入初始化的变量,不能是私有变量
      override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View {
        viewModel.xxx()
     }
 }

    比如这里,fragment中有一个viewModel,需要被初始化,给它添加@Inject注解。表示我们通过依赖注入的方式来初始化这个变量。这样就能够直接使用了吗?那肯定是不行的,因为还没有对GalleryViewModel进行声明。那么怎么对GalleryViewModel进行声明呢?

 @HiltViewModel
 class GalleryViewModel @Inject constructor(
     
 ) 

    需要在构造函数前面也加上@Inject注解,这样就可以了。这就是Hilt的最简单的用法,在我们项目中肯定不可能这么简单的使用的。因为在fragment中一般都会有特定的方法去初始化viewmodel,比如通过by viewModels()的方式去初始化viewmodel。而且一般来说,viewmodel的构造函数一般都是有参数的构造函数。因为mvvm中有一层Repository层,viewmodel取数据都是去Repository中取得。所以如果有参数的构造函数,那么应该如何去初始化?

有参构造函数的初始化

    比如GalleryViewModel中有一个UnsplashRepository类型的参数,如下。那么要如何去初始化呢?

 @HiltViewModel
 class GalleryViewModel @Inject constructor(
     private val repository: UnsplashRepository
 ) : ViewModel() {
     
 }

    仔细想想,刚才我们是给ViewModel提供了一个@Inject的注解,然后Hilt就自动去帮助我们去实例化viewModel了。现在的场景是什么?在初始化viewmodel的时候有一个参数,这个参数Hilt不知道哪里去实例化。viewmodel是依赖repository的,要对viewmodel通过依赖注入的方式实现初始化,那么Hilt就必须要知道怎么去初始化viewmodel里面的参数。仔细想想是不是这个道理?打个比方,我要做一台电脑(viewmodel),那么我必须知道主机,显示器等等(viewmodel参数)怎么做,我才能做出一台电脑。

    那现在应该怎么做?答案很简单,让Hilt知道怎么去初始化repository。刚刚是如何让hilt知道怎么初始化viewmodel的?是不是在构造函数的时候添加了@Inject注解?所以同理,那么只需要给repository的构造函数添加@Inject就可以了。

如下:

 class UnsplashRepository @Inject constructor() {}

    总结一下上面的有参构造函数的依赖注入:要对有参构造函数实现依赖注入,那么首先它的每一个参数要支持依赖注入的方式去初始化

Hilt模块

    看了上面的例子,有一些好奇的同学就想了,如果有参构造函数中的参数有接口类型的怎么办?我们不可能通过构造函数注入接口吧。还有,如果这个有参构造函数参数的类型是第三方的库怎么办?我们不可能改变让第三方库也支持依赖注入的构造函数吧。不着急,这些情况Hilt的开发者都有考虑到,并且对上面这些情况都做了支持。那就是利用Hilt模块去做。下面通过例子来讲解Hilt模块怎么使用

第三方类的初始化

    还是接着上面的这个例子继续讲解。我们知道Repository是负责提供数据的,这些数据有可能是本地数据,也有可能是网络数据。所以在初始化的Repository的时候肯定是有参数的。如果在UnsplashRepository的构造函数里,有一个参数,他是接口类型如下。

 class UnsplashRepository @Inject constructor(private val service: UnsplashService) {
     
 }
 ​
 interface UnsplashService {
 ​
     @GET("search/photos")
     suspend fun searchPhotos(
         @Query("query") query: String,
         @Query("page") page: Int,
         @Query("per_page") perPage: Int,
         @Query("client_id") clientId: String = BuildConfig.UNSPLASH_ACCESS_KEY
     ): UnsplashSearchResponse
 ​
     companion object {
         private const val BASE_URL = "https://api.unsplash.com/"
 ​
         fun create(): UnsplashService {
             val logger = HttpLoggingInterceptor().apply { level = Level.BASIC }
 ​
             val client = OkHttpClient.Builder()
                 .addInterceptor(logger)
                 .build()
 ​
             return Retrofit.Builder()
                 .baseUrl(BASE_URL)
                 .client(client)
                 .addConverterFactory(GsonConverterFactory.create())
                 .build()
                 .create(UnsplashService::class.java)
         }
     }
 }

    在UnsplashRepository中有一个依赖UnsplashService,这个依赖是interface类型的。所以不能通过上面有参构造函数的方式去初始化UnsplashService。这时候需要用Hilt模块去实现了。如下:

 @InstallIn(SingletonComponent::class)
 @Module
 class NetworkModule {
     @Singleton
     @Provides
     fun provideUnsplashService(): UnsplashService {
         return UnsplashService.create()
     }
 }

    这里有四个注解,下面 一一讲解。首先是@Module,他会告知Hilt如何提供某些实例类。但是还是需要@InstallIn这个注解,这个注解的意思是可以再哪些类中去使用这个NetworkModule,比方这里的SingletonComponent::class,他表示所有的类中都可以使用这个NetworkModule提供这里面的实例类。@Singleton,表示的是什么?他表示这个类的实例,只创造一次。你可以理解这个类为单例。最后一个@Provides注解,他主要向Hilt提供一下三点信息:

  • 函数返回类型会告知 Hilt 函数提供哪个类型的实例。比方这里返回类型就是UnsplashService
  • 函数参数会告知 Hilt 相应类型的依赖项。这里没有
  • 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体。比方这里是通过去执行UnsplashService.create()去初始化这个实例。UnsplashService.create()的代码在UnsplashService接口中有实现了。
有一个实现类的接口初始化化

    以上Hilt模块的一种使用,这一种使用了@Provides的一般是去初始化第三方库的时候去使用的。有的同学问了,如果这个接口是本地接口,而且有多个实现,应该如何去做呢?首先先看是本地接口,有一种实现类的情况

 interface UnsplashService{
     fun doSomething()
 }
 ​
 //构造函数需要添加@Inject注解,因为Hilt需要知道如何提供UnsplashServiceImpl实例
 class UnsplashServiceImpl @Inject constructor(...): UnsplashService {
     ....
 }
 ​
 @Module
 @InstallIn(SingletonComponent::class)
 abstract class UnsplashModule {
 ​
   @Binds
   abstract fun bindUnsplashService(
     unsplashServiceImpl: UnsplashServiceImpl
   ): UnsplashService
 }

    首先是UnsplashServiceImpl类,这是一个实现了UnsplashService接口的类,他需要用@Inject声明,因为Hilt需要知道如何提供。

    其次是UnsplashModule,这里声明的是一个抽象类,因为里面没有方法的具体实现。还有一个抽象方法bindUnsplashService。这个抽象方法的一个参数是UnsplashServiceImpl,返回值是UnsplashService因为要提供的是UnsplashService实例,所以返回值是UnsplashService。那提供的是哪个类型的实例呢?参数是什么类型最终提供的就是什么类型。 比如这里参数是UnsplashServiceImpl,所以最终提供的是UnsplashServiceImpl类型。注意这里是有@Binds注解的。@Binds注解就是实现上面功能的关键。

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

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

  • 函数返回类型会告知 Hilt 函数提供哪个接口的实例。
  • 函数参数会告知 Hilt 要提供哪种实现。
有多个实现类的接口的初始化

    还是接着上面的例子,现在接口UnsplashService还有一个实现类,叫UnsplashServiceImpl2如下:

 //构造函数需要添加@Inject注解,因为Hilt需要知道如何提供UnsplashServiceImpl2实例
 class UnsplashServiceImpl2 @Inject constructor(...): UnsplashService {
     ....
 }

    那如果需要提供UnsplashServiceImpl2的实例,该如何初始化?首先声明一点就是我们不能直接在

 @Module
 @InstallIn(SingletonComponent::class)
 abstract class UnsplashModule {
 ​
   @Binds
   abstract fun bindUnsplashService(
     unsplashServiceImpl: UnsplashServiceImpl
   ): UnsplashService
     
     @Binds
   abstract fun bindUnsplashService2(
     unsplashServiceImpl2: UnsplashServiceImpl2
   ): UnsplashService
 }

如果直接这样写的话,编译都编译不过, 因为Hilt也不知道这个模块到底是提供哪一个实例。遇到这种情况,就需要使用限定符了。限定符是一种注释,当为某个类型定义了多个绑定时,可以使用它来标识该类型的特定绑定。

首先先定义两个注解,一个是ServiceImpl一个是ServiceTwo

 @Qualifier
 @Retention(AnnotationRetention.BINARY)
 annotation class ServiceImpl
 ​
 @Qualifier
 @Retention(AnnotationRetention.BINARY)
 annotation class ServiceTwo

    有了这两个注解,那么回到UnsplashModule中就可以直接使用了

 @Module
 @InstallIn(SingletonComponent::class)
 abstract class UnsplashModule {
 ​
   @ServiceImpl
   @Binds
   abstract fun bindUnsplashService(
     unsplashServiceImpl: UnsplashServiceImpl
   ): UnsplashService
     
     @ServiceTwo
     @Binds
   abstract fun bindUnsplashService2(
     unsplashServiceImpl2: UnsplashServiceImpl2
   ): UnsplashService
 }

    注意,还没有完,为什么呢?因为Hilt这里只是知道了该如何提供实例,那什么情况下提供哪一个呢?Hilt还是不知道。所以还需要在我们去明确什么时候注入哪一个?在哪里去明确。回到UnsplashRepository这个类里面,我们这里是没有明确需要哪一个注入的,怎么明确?添加我们自定义的注解即可

 class UnsplashRepository @Inject constructor(@ServiceTwo private val service: UnsplashService) {
     
 }

    通过以上的步骤就可以完成依赖注入了。

为 Android 类生成的组件

    上面讲解了这么多,都是讲解怎么去使用Hilt,没有讲Hilt组件还有Hilt组件的生命周期。来看继续看一下@InstallIn这个注解,它后面跟了一个组件 SingletonComponent::class,这个SingletonComponent是Hilt其中一个组件,Hilt提供了以下组件,每一个组件都有它注入面向的对象,什么意思,比如上面这个@InstallIn(SingletonComponent::class),那么就是在所有的对象中,都可以使用这个模块去注入依赖。

Hilt 组件注入器面向的对象
SingletonComponentApplication
ActivityRetainedComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponent带有 @WithFragmentBindings 注释的 View
ServiceComponentService

组件生命周期

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

生成的组件创建时机销毁时机
SingletonComponentApplication#onCreate()Application#onDestroy()
ActivityRetainedComponentActivity#onCreate()Activity#onDestroy()
ActivityComponentActivity#onCreate()Activity#onDestroy()
FragmentComponentFragment#onAttach()Fragment#onDestroy()
ViewComponentView#super()视图销毁时
ViewWithFragmentComponentView#super()视图销毁时
ServiceComponentService#onCreate()Service#onDestroy()

组件作用域

    默认情况下,Hilt 中的所有绑定都未限定作用域。这意味着,每当应用请求绑定时,Hilt 都会创建所需类型的一个新实例。

    不过,Hilt 也允许将绑定的作用域限定为特定组件。Hilt 只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。

    下表列出了生成的每个组件的作用域注释:

Android 类生成的组件作用域
ApplicationSingletonComponent@Singleton
ViewModelActivityRetainedComponent@ActivityRetainedScope
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
带有 @WithFragmentBindings 注释的 ViewViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScoped
 @ActivityScoped
 class UnsplashAdapter @Inject constructor(
   private val service: UnsplashService
 ) { ... }

在上面这个例子中,使用 @ActivityScopedUnsplashAdapter 的作用域限定为 ActivityComponent,Hilt 会在相应 Activity 的整个生命周期内提供 UnsplashAdapter 的同一实例

注意:将绑定的作用域限定为某个组件的成本可能很高,因为提供的对象在该组件被销毁之前一直保留在内存中。请在应用中尽量少用限定作用域的绑定。如果绑定的内部状态要求在某一作用域内使用同一实例,或者绑定的创建成本很高,那么将绑定的作用域限定为某个组件是一种恰当的做法。

Hilt 中的预定义限定符

另外Hilt 提供了一些预定义的限定符。例如,由于您可能需要来自应用或 Activity 的 Context 类,因此 Hilt 提供了 @ApplicationContext@ActivityContext 限定符。

假设本例中的 UnsplashRepository 类需要 Activity 的上下文。以下代码演示了如何向 UnsplashRepository 提供 Activity 上下文:

 class UnsplashRepository @Inject constructor( @ActivityContext private val context: Context,@ServiceTwo private val service: UnsplashService) {
     
 }

每个 Hilt 组件都附带一组默认绑定,Hilt 可以将其作为依赖项注入您自己的自定义绑定请注意,这些绑定对应于常规 Activity 和 Fragment 类型,而不对应于任何特定子类。这是因为,Hilt 会使用单个 Activity 组件定义来注入所有 Activity。每个 Activity 都有此组件的不同实例。

Android 组件默认绑定
SingletonComponentApplication
ActivityRetainedComponentApplication
ActivityComponentApplicationActivity
FragmentComponentApplicationActivityFragment
ViewComponentApplicationActivityView
ViewWithFragmentComponentApplicationActivityFragmentView
ServiceComponentApplicationService
 class UnsplashRepository @Inject constructor( val activity :Activity,@ServiceTwo private val service: UnsplashService) {}

以上是能够正确编译的。

组件层次结构

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

hilt-hierarchy.svg

结尾

    这是一篇关于Hilt的文章,Hilt的知识点,对于第一次接触依赖注入的同学来说可能比较难以掌握,但是没有关系,只要多看几遍,并结合文章中的例子讲解,尝试在项目中使用,很快就能够掌握了。文中例子主要是参考github.com/android/sun…里面改写的。有兴趣的可以去看看源码,并且尝试改动运行一下。如果文中有不对的地方,欢迎指正。