玩转Jetpack依赖注入框架——Hilt

3,050 阅读5分钟

什么是依赖注入

依赖注入的英文是Dependency Injection,简称DI,做过Java开发的读者可能知道,Spring 框架中的控制反转功能就是通过依赖注入的方式来实现的。有个很有趣的现象,当我与许多Android开发者交流依赖注入技术的时候,他们总是连忙回避说:“依赖注入太难用了,我从来没有使用过依赖注入”。

真的是这样吗?事实是99%的Android开发者都在项目中使用过依赖注入却没有意识到,那么什么是依赖注入呢?

简单的说,一个类中使用的依赖类不是类本身创建的,而是通过构造函数或者属性方法设置的,这种实现方式就称为依赖注入。以手机需要插入SIM卡才可以正常拨打电话为例,手机需要依赖SIM卡。这里新建MobilePhone类和SimCard类,不使用依赖注入的实现方式,代码如下所示:

class SimCard {
    private val TAG = "SimCard"
    fun dialNumber() {
        Log.d(TAG, "拨打电话")
    }
}
class MobilePhone {
    fun dialNumber() {
        val simCard = SimCard()
        simCard.dialNumber()
    }
}

接着就可以调用MobilePhone类中的拨打电话方法了,代码如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val mobilePhone = MobilePhone()
    mobilePhone.dialNumber()
}

通过上面得代码可以知道,当调用MobilePhone的dialNumber方法时,首先在MobilePhone类的dialNumber方法中创建了SimCard对象,然后调用SimCard对象的dialNumber方法。这种实现方式MobilePhone类虽然依赖SimCard类,但使用时依赖类是MobilePhone类自身创建的,所以这种实现方式并没有使用依赖注入去实现。上面的例子如果使用依赖注入又该如何实现呢?

很简单,首先修改MobilePhone类的dialNumber方法,代码如下所示:

class MobilePhone {
    fun dialNumber(simCard: SimCard) {
        simCard.dialNumber()
    }
}

这里为dialNumber方法添加了一个SimCard类型的参数,使用参数调用SimCard类的dialNumber方法,Activity中的代码如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val mobilePhone = MobilePhone()
    val simCard = SimCard()
    mobilePhone.dialNumber(simCard)
}

在MainActivity中创建了SimCard类的实例,传给MobilePhone类的dialNumber方法使用,这种实现方式就是依赖注入。

在普通的实现方式中,MobilePhone类不仅要负责自身类的功能,还要负责创建SimCard类,在MainActivity中创建MobilePhone类也是一样。使用依赖注入不仅可以提高代码的可扩展性,还可以分离依赖项。上面演示代码中的依赖注入方式只是将依赖项的创建时机放到了更上层,在实际开发中,类的依赖关系较为复杂,如果仍使用示例中的依赖注入方式就不太合适了。Hilt是Google官方为开发者提供的可以简化使用的依赖注入框架。Hilt又是在Dagger的基础上开发的,所以在开始了解Hilt组件的使用方式之前,不得不来介绍一下Hilt与Dagger的关系。

从Dagger看Hilt

Dagger是Square公司开发的一个依赖注入框架。Dagger最初版本是采用反射的方式去实现的,相信开发者都知道,过多使用反射方法会影响程序的运行效率。由于反射方法在编译阶段是不会产生错误的,导致只有在程序运行时才可以验证反射方法是否正确。考虑到上述问题,Google基于Dagger开发了Dagger2,Dagger2是通过注解的方式去实现的,如此在编译时就可以发现依赖注入使用的问题。但Dagger2使用起来是比较繁琐的,因此可以掌握Dagger2并熟练使用的开发者并不多。而Hilt组件是基于Dagger开发、专门面向Android开发者的依赖注入框架,所以Hilt只是为依赖注入提供了更简便的实现方式,而不是提供了依赖注入的能力。那么Hilt又该如何使用呢?

Hilt的基本使用

添加依赖

相比较Jetpack其他组件而言,添加Hilt依赖项还是稍微有些复杂的。首先在根项目的build.gradle中添加Hilt插件,示例代码如下所示:

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

然后在app模块下的build.gradle中添加如下依赖项,代码如下所示:

plugins {
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}
dependencies {
...
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Hilt当前支持的Android类及其注解与注意事项如表所示。

Android类注解注意事项
Application@HiltAndroidApp必须定义一个Application
Activity@AndroidEntryPoint仅支持扩展ComponentActivity的Activity
Fragment@AndroidEntryPoint仅支持扩展androidx.Fragment的Fragment
View@AndroidEntryPoint/
Service@AndroidEntryPoint/
BroadcastReceiver@AndroidEntryPoint/

每个应用程序都包含一个Application,开发者可以通过自定义Application来做一些基础的初始化等操作。在使用Hilt时,开发者必须自定义一个Application,并为其添加@HiltAndroidApp注解。这里新建BaseApplication类继承自Application,并为其添加@HiltAndroidApp注解,代码如下所示:

@HiltAndroidApp
class BaseApplication : Application() {
}

将BaseApplication注册到配置文件中,代码如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example1.hiltdemo">
    <application
        android:name="com.example.BaseApplication"
...

这些准备工作做好后,首先来看如何使用Hilt注入普通的对象。

依赖注入普通对象

新建UserManager类,提供获取Token的方法,代码如下所示:

class UserManager {
    val TAG = "UserManager"
    fun getUserToken() {
        Log.d(TAG, "获取用户token")
    }
}

当Activity中需要获取Token时,编写代码如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
  ...
  val userManager = UserManager()
  userManager .getUserToken()
 }

运行程序,打印日志如图所示。

上面的功能虽然可以正常运行,但所存在的问题也是一目了然的。MainActivity不仅负责UI的显示,还创建了UserManager类。如果MainActivity的依赖类过多会导致MainActivity臃肿且难以维护,这个时候Hilt就该上场了。

Hilt通过为被依赖类的构造函数添加@Inject注解,来告知Hilt如何提供该类的实例,修改UserManager类,代码如下所示:

class UserManager @Inject constructor() {
    val TAG = "UserManager"
    fun getUserToken() {
        Log.d(TAG, "获取用户token")
    }
}

接着为MainActivity中的UserManager依赖注入,代码如下所示:

 @AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
 lateinit  var userManager: UserManager
  override fun onCreate(savedInstanceState: Bundle?) {
...
    user.getUserToken()
    }
}

代码中首先为MainActivity添加 @AndroidEntryPoint注解,声明一个延迟初始化的UserManager变量并添加@Inject注解。运行程序,运行结果与图一致,这样MainActivity就通过依赖注入获取到了UserManager类的实例。这里需要注意的是:由Hilt注入的字段如这里的userManager不能为私有类型,否则会在编译阶段产生错误。

有些业务中要注入的对象可能存在参数,如8.1小节所示的MobilePhone类需要依赖SimCard类,这样就不能直接在MainActivity中注入MobilePhone类了。其实解决这个问题也很简单,MainActivity依赖MobilePhone类,MobilePhone类又依赖SimCard类,如果想让MobilePhone类依赖注入,则SimCard类也必须依赖注入才可以,修改SimCard、MobilePhone类代码如下所示:

class MobilePhone @Inject  constructor ( val simCard: SimCard) {
    fun dialNumber() {
        simCard.dialNumber()
    }
}
class SimCard @Inject  constructor ( ) {
    private val TAG = "SimCard"
    fun dialNumber() {
        Log.d(TAG, "拨打电话")
    }
}

在MainActivity中注入MobilePhone,并调用dialNumber方法,代码如下所示:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var mobilePhone: MobilePhone
    override fun onCreate(savedInstanceState: Bundle?) {
...
        mobilePhone.dialNumber()
    }
}

运行程序,结果如图所示。

如此一来就实现了带参数的依赖注入,但仍有些外部类无法通过构造函数去注入,如经常使用的OkHttp等第三方开源库,接下来来看如何依赖注入第三方开源库。

依赖注入第三方组件

以OkHttp实现为例,这里就不讲解OkHttp的基础使用了,如果有不了解OkHttp的读者,可自行通过官网学习。使用OkHttp发起网络请求时会创建OkHttpClient实例,编写代码如下所示:

var okHttpClient = OkHttpClient.Builder()
   .connectTimeout(10, TimeUnit.SECONDS)
   .build()

但是我们不能将这部分代码字节写在Activity中,应当使用依赖注入的方式为MainActivity注入OkHttpClient对象。由于OkHttpClient类是第三方库的类导致开发者无法直接添加注解,这里首先新建NetWorkUtil类,并添加@Module与@InstallIn注解,代码如下所示:

@Module
@InstallIn(ActivityComponent::class)
class NetWorkUtil {}

@Module注解表示这是一个用于提供依赖注入实例的模块。@InstallIn注解表示要装载到哪个模块中。这里使用ActivityComponent表示要装载到Activity组件中,所以开发者可以在Activity、Fragment以及View中使用NetWorkUtil模块,如果还想在这三个组件之外使用NetWorkUtil模块,则需要装载到其他组件中,Hilt组件类型与注入场景以及生命周期对应关系如表所示。

组件名称注入场景生命周期
ApplicationComponentApplicationApplication#onCreate() ~ Application#onDestroy()
ActivityRetainedComponentViewModelActivity#onCreate() ~ Activity#onDestroy()
ActivityComponentActivityActivity#onCreate() ~ Activity#OnDestroy()
FragmentComponentFragmentFragment#onAttach() ~ Fragment#onDestroy()
ViewComponentViewView#super() ~ 视图销毁
ViewWithFragmentComponent@WithFragmentBindings 注解的ViewView#super() ~ 视图销毁
ServiceComponentServiceService#onCreate() ~ Service#onDestroy()

如果想在应用全局中使用NetWorkUtil模块,则将InstallIn注解属性值修改为ApplicationComponent::class即可。

然后我们在NetWorkUtil类中新增getOkHttpClient方法代码如下所示:

@Provides
fun getOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}

这里使用@Provides注解提供获取方法,这里的方法名getOkHttpClient可以任意取,不影响使用,现在如果想在Activity中使用OkHttpClient可编写代码如下所示:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var okHttpClient: OkHttpClient
    override fun onCreate(savedInstanceState: Bundle?) {
        ... 
        okHttpClient.newCall(request).enqueue(object : Callback {
              ...
        })
    }
}

这样开发者就不需要在Activity中创建OkHttpClient对象了,不过现在的代码还是存在问题的。一般情况下开发者都会将OkHttpClient对象设置为单例模式的,即全局只有一个OkHttpClient对象,解决这个问题需要将InstallIn属性值设置为ApplicationComponent::class并且为getOkHttpClient方法添加@Singleton注解,修改后的代码如下所示:

@Module
@InstallIn(ApplicationComponent::class)
class NetWorkUtil {
 @Singleton
 @Provides
 fun getOkHttpClient(): OkHttpClient {
      var okHttpClient = OkHttpClient.Builder()
           .connectTimeout(10, TimeUnit.SECONDS)
           .build()
      return okHttpClient
  }
}

这样在任意地方调用getOkHttpClient方法都只会创建一个OkHttpClient对象。@Singleton注解是Application组件类的作用域,Hilt只为绑定作用域限定到的组件的每个实例,创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。各组件对应作用域关系如表所示。

组件名称作用域
ApplicationComponent@Singleton
ActivityRetainedComponent@ActivityRetainedScope
ActivityComponent@ActivityScoped
FragmentComponent@FragmentScoped
ViewComponent@ViewScoped
ViewWithFragmentComponent@ViewScoped
ServiceComponent@ServiceScoped

这里需要注意的是,绑定的作用域必须与其安装到的组件的作用域一致,否则在运行程序时会发生异常。

上面代码创建的OkHttpClient对象设置的超时时间是10秒钟,在实际业务开发中可能还会配置许多其他属性,如添加拦截器等,这里以修改超时时间是20秒钟为例,该如何再提供一个超时时间为20秒钟的OkHttpClient对象呢?

同样的先添加一个getOtherOkHttpClient方法,并将超时时间设置为20秒钟,代码如下所示:

@Provides
fun getOtherOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}

运行程序,出现编译错误,错误如图所示。

这个错误开发者也可以理解,由于在程序中声明了两个提供OkHttpClient实例的方法,在使用的时候Hilt并不知道要依赖注入哪个实例,这个时候就要用到Qualifier注解来解决这个问题了。Qualifier注解的作用就是为相同类型的类注入不同的实例,一起来看看具体该如何实现。

新建QualifierConfig文件,代码如下所示:

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

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

@Retention是用于声明注解的作用范围,这里声明为AnnotationRetention.BINARY表示注解在编辑后将会被保留。这里定义了两个OkHttpClientStandard和OtherOkHttpClient两个注解类,定义好后,将注解类使用在提供OkHttpClient实例的方法即可,代码如下所示:

@Singleton
@OkHttpClientStandard
@Provides
fun getOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}

@OtherOkHttpClient
@Provides
fun getOtherOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}

在Activity中使用两个OkHttpClient实例的方法如下所示:

@OkHttpClientStandard
@Inject
lateinit var okHttpClient: OkHttpClient

@OkHttpClientStandard
@Inject
lateinit var otherOkHttpClient: OkHttpClient

相信读者对Hilt如何依赖注入普通类和第三方类,已经非常了解了。此外,Hilt还集成了Jetpack组件,接下来来看Hilt如何依赖注入架构组件。

依赖注入架构组件

当前Hilt仅支持ViewModel组件和WorkManager组件,这里以ViewModel组件来看如何使用Hilt依赖注入ViewModel?ViewModel依赖及基础使用方法可参照第三章的内容。

首先在build.gradle中添加Hilt的扩展依赖,代码如下所示:

dependencies {
...
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
}

新建一个MainViewModel,并为其构造方法添加@ViewModelInject注解,代码如下所示:

class MainViewModel @ViewModelInject constructor(
) : ViewModel() {}

这样,就可以通过和之前一样的方法来获取ViewModel对象了,代码如下所示:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    val mainViewModel by viewModels<MainViewModel>()
...
}

这里可以使用和依赖注入之前使用一样的方式获取ViewModel的对象,都是Hilt自动为开发者处理好的,当然也可以使用依赖注入普通类的方式注入ViewModel的对象,代码如下所示:

class MainViewModel @Inject constructor(
) : ViewModel() {}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var mainViewModel: MainViewModel
    ...
}

但是这种方式改变了获取ViewModel的正常方式并不建议使用。这样就实现了Hilt依赖注入ViewModel的功能。