白话Android Jetpack新成员:Hilt依赖注入(Dependency Injection)

1 依赖注入(Dependency Injection)?

依赖注入(Dependency Injection),简称DI,并不是一个新概念,是软件工程学里早就存在的名词。在维基百科上对于依赖注入DI的定义是:

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies. In the typical “using” relationship the receiving object is called a client and the passed (that is, “injected”) object is called a service.

简单来说,依赖注入就是一个对象A需要(依赖于)另外一个对象B的实例。需要把对象B的实例注入到对象A中,而这个“注入”的动作,要么由程序员手动执行,要么交给软件框架执行;例如我们今天的主角Hilt,就是一个功能强大,简单易用的依赖注入框架。

这么说可能有点抽象,我们来看下面这个以汽车为例的例子(以下代码片段使用Kotlin语言来表述,入门可参考Kotlin中文官网或者郭霖的新书《第一行代码 Android 第3版》):

汽车里需要一个引擎(实例),在不用依赖注入时,我们只能在汽车(类)里面new一个引擎(实例)出来:private val engine = Engine()

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

这样写会有什么问题呢?

  • 汽车Car类和引擎Engine严重耦合。每个Car类的实例都使用不同的Engine实例。
  • 这种汽车Car类内部“建造引擎”的方式,使得该汽车Car类难以测试及维护。比如现在电动汽车很火热,我们需要把汽车区分为内燃机汽车和电动汽车时,只能去Car类的内部修改。这就违反了面向对象软件开发的“开放封闭原则”。

那么“依赖注入风格”的代码应该是什么样的呢? Car类在它的构造函数里接收一个Engine类型的参数:class Car(private val engine: Engine){ ... }

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

这样汽车Car类就不需要自己内部“建造引擎”,它需要什么样的引擎,直接在实例化的时候从外部传(注入)进来就可以了。

在这里,我们就可以说Car类的实例化依赖Engine类的实例,我们在构造Car类实例car(实例对象变量一般首字母小写)的时候传入(注入)了Engine类的实例。

以上的代码片段,就是一个典型的“手动依赖注入(DI)”场景。其好处也是显而易见的:

  • Car类可重复使用,你需要一辆燃油车,在实例化的时候传入内燃机引擎;而当需要一辆电动车的时候,传入电动机引擎;
  • Car类的可测试性变成了现实(你可以传入不同类型的FakeEngine做测试 Test Double)。

以上通过类的构造函数传入参数,是实现依赖注入的其中一种方式,另外一种方式叫做类字段注入(Field Injection (or Setter Injection)),如下代码片段所示中的lateinit var engine: Engine

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

大家可能觉得奇怪,通过构造函数传参数的方式,把接口暴露给外部,不是更直接明了吗?为什么还需要这种类字段注入(Field Injection)的隐藏方式呢?

那是因为有些Android系统框架的类,例如 activity 类和 fragment 类,我们又不能去修改它们的构造函数。通过Field Injection的方式,在这些类里面就可以实现依赖注入了。

通过以上的分析,我们对依赖注入(DI)有了一个相对直观的印象。依赖注入(DI)的目的就是为了代码间的解耦,解耦的目的是为了提高代码的可重用性可测试性,而提高了代码的可重用性和可测试性是为了增强代码的健壮性(Robust)

同时依赖注入有两种实现方式:

  • 构造函数注入(Constructor Injection),主要用于自己写的类实现;
  • 类字段注入(Field Injection),用于Android系统级别或者第三方框架的类实现;

下面将目光回到我们今天的主角,Hilt


2 Hilt 如何实现依赖注入?

用Android开发者官网关于Hilt介绍的原话来说就是:

Hilt provides a standard way to use DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically.

简单来说就是Hilt干了两件事:1,提供了“containers”(容器)用来装各种依赖;2,并自动管理这些“containers”(容器)的“lifecycles”(生命周期)。

前面我们刚提到,依赖注入有两种实现方式:1, 构造函数注入(Constructor Injection);2,类字段注入(Field Injection)

但是从Hilt的介绍里,似乎丝毫看不到跟这两种实现方式有关联的地方?

别着急,为了更好地理解Hilt是怎样快捷、高效地帮我们实现Android开发的依赖注入,我们得先从“手动实现依赖注入”开始。


3 如何手动依赖注入?

手动依赖注入(Manual dependency injection

我们先来看一张在Android开发者官网推荐的android应用架构图(也称MVVM架构图)。

如果对Android的MVVM架构和Jetpack组件没了解过,可以参考Android开发者官网关于Jetpack组件的说明,或者郭霖的新书《第一行代码 Android 第3版》第13章和第15章。

也可以直接到github上看Jetpack的最佳实践Android Sunflower项目。
Android's recommended app architecture
上图中的箭头是单方向的,意思就是箭头的一端依赖于另外一端。

比如Activity/Fragment依赖于ViewModel,而ViewModel依赖于Repository。

在Android MVVM架构里,依赖注入的意思,就是把ViewModel的实例(instance)注入到Activity/Fragment类中,同样的道理,Repository的实例(instance)注入到ViewModel类中。以此类推,Model和RemoteDataSource的实例也需要注入到Repository类中。

更通俗(啰嗦)一点的说法,因为在Activity/Fragment 类(class)中需要用到ViewModel的实例(instance),所以我们要想办法把ViewModel的实例(instance)注入到Activity/Fragment 类(class)中。

可能会有些读者想:在哪需要实例(instance),直接new(生成)一个不就完了么,干嘛费那么大的劲呢?

我们在前面的文章中已经说过,这里再次明确:依赖注入(DI)的目的是:解耦

框架一定程度目的就是解耦(或者比较时髦的叫法“关注点分离(Separation of concerns,SOC)”)。

解耦是为了啥呢?这里再次强调一遍:

1. 代码可复用;
2. 代码可重构;
3. 代码可测试。


接下来我们再来看下面这个具体的例子:

比如现在需要实现一个用户登录的功能时,用MVVM架构应该是下面这样的:

Login Feature MVVM
上图的单箭头跟前面 Android MVVM架构图里的单箭头意义是一样的——前者依赖于后者。

LoginActivity依赖于LoginViewModel, LoginViewModel依赖于UserRepository, UserRepository依赖于UserLocalDataSourceUserRemoteDataSource。最后,UserRemoteDataSource依赖于Retrofit。(Retrofit是一个很有名的Android网络请求库)

这些类的功能从其名称上看就比较直观了,LoginActivity是用于处理用户登录的一个Activity,它里面需要有一个LoginViewModel的实例。

在没有使用依赖注入思想的时候,直观上我们认为LoginActivity应该如下代码段所示:

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 实例化LoginViewModel
        loginViewModel = LoginViewModel(userRepository)
    }
}

为了满足LoginViewModel的实例化,我们需要传入一个UserRepository的实例参数userRepository

LoginViewModel类, UserRepository类,UserLocalDataSource类和UserRemoteDataSource类大概长这个样子:

class LoginViewModel(
    private val userRepository: UserRepository,
) { ... }

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource { ... }

class UserRemoteDataSource(
    private val loginService: LoginRetrofitService
) { ... }

所以,回到LoginActivity类,为了得到loginViewModel对象,我们需要增加以下代码:

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

		/******新增代码 Begin ******/
        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()

        val userRepository = UserRepository(localDataSource, remoteDataSource)
		/******新增代码 End ******/
		
        loginViewModel = LoginViewModel(userRepository)
    }
}

在以上的新增代码中,我们分别按照图中箭头的方向,按照相应的依赖关系,实例化了Retrofit类,UserRemoteDataSource类,UserLocalDataSource类,UserRepository类。

到此,LoginActivity类才可以正常使用,但是跟我们最初设想的在LoginActivity类中只要有loginViewModel对象就行,其他的不需要出现在LoginActivity类中。我们需要的类似是如下干净的代码:

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        loginViewModel = XXXX
    }
}

我们要怎么做才能达到上面简洁的代码呢?

可以新建一个AppContainer类,把之前新增的代码都扔到里面去:

class AppContainer {

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

这样新建的AppContainer类还不可以在LoginActivity类中使用,因为AppContainer类中的这些“依赖”,需要在整个应用(application )全局中使用,所以需要把AppContainer类的实例放到Application()的子类中:

我们新建一个MyApplication类,继承自Application()

class MyApplication : Application() {
    val appContainer = AppContainer()
}

这样一来,在LoginActivity中就可以这么用了:

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)        
    }
}

到此为止,在LoginActivity类中已经可以看出依赖注入的编码风格了。

我们在这里实现的loginViewModel对象,使用的就是构造函数注入(Constructor Injection)的依赖注入方式:loginViewModel = LoginViewModel(appContainer.userRepository)

还记得前文提到的Hilt干了哪两件事吗?

  1. 提供了“containers”(容器)用来装各种依赖;
  2. 并自动管理这些“containers”(容器)的“lifecycles”(生命周期)。

这个Hilt中提供的“container”,与我们自己建立的AppContainer其实是一个意思。都是用来装各种“依赖”的。


3.1 更完善一点的依赖注入?

假如在我们的Android应用程序中,除了LoginActivity类,其他类也需要LoginViewModel的实例对象,那么我们就不能在LoginActivity类中新建LoginViewModel的实例对象了。

还是老办法,作为其中的一个“依赖”,我们要把实现LoginViewModel的实例对象放到“containers”(容器)中。

在此,需要用到工厂模式的设计模式,新建一个Factory<T>接口,然后在LoginViewModelFactory类中实现这个接口,并返回一个LoginViewModel的实例:

interface Factory<T> {
    fun create(): T
}

class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
    override fun create(): LoginViewModel {
        return LoginViewModel(userRepository)
    }
}

接下来的事情就简单了,我们把这个新建的LoginViewModelFactory工厂类,放到AppContainer中:

class AppContainer {

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)

	// 新建一个 loginViewModelFactory 对象,在整个application范围内都可以使用
 	val loginViewModelFactory = LoginViewModelFactory(userRepository)	
}

然后在LoginActivity类中就可以直接使用了(而不是像之前那样,新建一个LoginViewModel的实例对象):

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        val appContainer = (application as MyApplication).appContainer
        //loginViewModel = LoginViewModel(appContainer.userRepository) 替换成下面代码:   
        loginViewModel = appContainer.loginViewModelFactory.create()    
    }
}

到此为止,你已经有了一个比较干净的LoginActivity类,而且loginViewModel对象也可以在application的其他地方使用。看起来这个“手动依赖注入”已经颇具完善性了。

不过我们回过头想想,Hilt干了哪两件事?

  1. 提供了“containers”(容器)用来装各种依赖;
  2. 并自动管理这些“containers”(容器)的“lifecycles”(生命周期)。

我们目前只手动实现了containers,也就是我们建立的AppContainer类。那还有另外关于lifecycles的那件事情呢?

3.2 手动管理lifecycles

现在很多Android应用都支持多用户,所以我们需要扩充上面的应用功能:记录不同的用户登录信息。

这样,需要在LoginActivity中增加新的功能,达到下面的目的:

  • 在这个用户登录期间保持对LoginUserData类实例对象的访问,退出登录后释放资源;
  • 当新用户登录后,重新新建一个LoginUserData类实例对象。

这时我们需要添加一个LoginContainer类,用来存储LoginUserData类的实例对象和LoginViewModelFactory类的实例对象。(TIPS: container 中放的是依赖,也就是各种类的实例对象)

class LoginContainer(val userRepository: UserRepository) {

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

然后把LoginContainer应用到之前的AppContainer中:

class AppContainer {

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
    
 	//val loginViewModelFactory = LoginViewModelFactory(userRepository)	
 	// loginViewModelFactory 的实现已经放到LoginContainer中,此处不再需要
 	
    // 新建一个loginContainer变量,类型是LoginContainer,初始值是null
    // 因为当用户退出时,其值时null
    var loginContainer: LoginContainer? = null	
}

接下来回到LoginActivity类中,我们需要在LoginActivity类的onCreate()阶段(用户登录),通过loginContainer拿到LoginUserData的实例对象,而在onDestroy()阶段(用户退出)释放相应的资源:

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel
    private lateinit var loginData: LoginUserData
    private lateinit var appContainer: AppContainer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        appContainer = (application as MyApplication).appContainer
        //loginViewModel = LoginViewModel(appContainer.userRepository) 替换成下面代码:   
        //loginViewModel = appContainer.loginViewModelFactory.create() 替换成下面代码:   

        // 用户登录,实例化LoginContainer,得到appContainer中的loginContainer 对象
        appContainer.loginContainer = LoginContainer(appContainer.userRepository)

		// loginViewModel 对象的获取比原来多了一层 loginContainer 对象
        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
        
        loginData = appContainer.loginContainer.loginData
    }

    override fun onDestroy() {
        // 用户退出,释放资源
        appContainer.loginContainer = null
        super.onDestroy()
    }
}

最后,我们终于通过手动依赖注入的方式,实现了我们要达到的代码解耦的目的。

现在我们来回顾一下,我们都做了哪些工作:

  1. 首先新建了一个AppContainer类,把LoginViewModel需要的各种依赖一股脑都放进去;
  2. 为了在应用的其他地方(除了LoginActivity以外)使用LoginViewModel的实例,我们用工厂类的设计模式在AppContainer类容器中实现了一个loginViewModelFactory对象;
  3. 最后为了实现不同用户的登录和登出,我们又新建了一个LoginContainer类容器,并放到AppContainer类容器中,然后在LoginActivity中可以在onCreate()中拿到用户登录的信息loginData,并且在用户登出以后,即在LoginActivity的onDestroy()中释放资源。

这么看来好像手动依赖注入似乎也还不是特别复杂,但是当我们的应用功能越来越复杂的时候(现在只是其中的一个登陆功能),手动依赖注入将会变得不可维护。

我们不仅需要编写很多模板代码(boilerplate code),还需要在适当的时候释放资源,否则很容易引发内存泄漏的风险。

以上的问题,对程序员来说,都不是那么令人愉悦:费力不讨好

这一切的烦恼,交给Hilt以后,都会迎刃而解。


4 使用Hilt实现优雅的依赖注入

Hilt 是建立在Dagger基础上的依赖注入框架,目的是为了在Android开发中更容易使用依赖注入,所以理论上我们只要学会使用Hilt ,而不需要去再次学习Dagger。

理由似乎也很简单,Hilt的出现就是为了让大家更方便地使用依赖注入,这也从侧面说明Dagger的使用并不是那么友好。不然谷歌也不会费劲再去开发Hilt了。

4.1 在工程中引入Hilt

第一步,在工程项目根目录的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 和 **data binding**功能的话,Android Studio软件版本需要升级到 4.0及以上。

最后一步,Hilt使用Java 8,所以还需要添加:

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

4.2 使用Hilt的准备工作

在我们的Android工程项目中,如果要使用Hilt,必须要有一个自定义的Application才行。

同时在该类上方标注@HiltAndroidApp

有了@HiltAndroidApp注解后,才会触发Hilt的代码生成功能,包括生成一个应用级别(application-level)的container(对,就是我们上文常说的那种container)。

这里我们新建了一个ExampleApplication类,继承自Application()

@HiltAndroidApp
class ExampleApplication : Application()

同时别忘记在AndroidManifest.xml文件中注册

    <application
        android:name=".ExampleApplication "
        ...>
    </application>

4.3 Android系统类的Hilt使用

还记得我们前面说过的依赖注入的两种实现方式吗?

  • 构造函数注入(Constructor Injection),只要用于自己写的类实现;
  • 类字段注入(Field Injection),用于Android系统级别或者第三方框架的类实现;

既然是Android系统类,那肯定只能使用第二种方式了。但是怎么样才能让Hilt知道谁才是Android系统类呢?答案就是在类前面加上注解@AndroidEntryPoint,例如Activity类如下:

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

目前,Hilt支持的Android系统类有以下六种:

  1. Application(使用@HiltAndroidApp注解标注)
  2. Activity
  3. Fragment
  4. View
  5. Service
  6. BroadcastReceiver

除了第一种的Application类使用@HiltAndroidApp以外,其他5类都使用@AndroidEntryPoint注解。

添加了@AndroidEntryPoint注解后,Hilt将会为每个Android系统类生成一个Hilt component(组件)(这里Hilt使用component关键字,而不是container,个人猜想有可能是为了和Hilt的最高层应用级别的 application-level container 区分开来,但是表达的意义应该是一致的)。

Android系统类对应着不同的Hilt component(组件),这些component(组件)将会存放对应的类需要的依赖,并且这些component(组件)是有层级(hierarchy)关系的。这些关系我们暂时不用去管,后续将会详细说明。

先来看看我们在Activity类中怎么注入一个依赖:

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

咋一看似乎跟我们在做手动依赖注入的时候不太一样。

我们再来看看在手动依赖注入的时候我们是如何声明变量的:

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel
	...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
		...
        loginData = appContainer.loginContainer.loginData
    }
}

Hilt的依赖注入没有了手动注入时的private关键字,增加了一个@inject注解。

但是他们的本质都一样,都属于依赖注入两者其一的“filed injection”(另外一种是constructor injection)。Activity是Android系统类,当然只能用filed injection

这里要明确说明一点,在使用Hilt做filed injection的时候,变量名前面是不能加private关键字的。因为加了private之后,这个变量就变成了类私有的变量,Hilt就没有权限访问了,也无从谈起依赖注入了。

这也就是我们在使用了Hilt之后,不需要像手动依赖注入时那样,在Activity类里面的onCreate()函数对这个变量进行赋值。而是使用@inject注解这个变量以后,直接在代码里使用,赋值的事情,是由Hilt帮我们代劳了。

不过读者可能会有疑虑,我们在ExampleActivity类里面只声明了变量的类型(@Inject lateinit var analytics: AnalyticsAdapter)为AnalyticsAdapter类,Hilt怎么才能知道去哪里找对应的AnalyticsAdapter类呢?

这个疑虑是对的,到目前为止,Hilt并不知道去哪找AnalyticsAdapter类,我们需要用某种方法告诉Hilt。这个方法很简单,就是在这个AnalyticsAdapter类的构造函数前也加上@injetct注解,如下代码:

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

是不是看起来有那么点“似曾相识”?对,这种依赖注入方式,就是constructor injection,前面在Activity中,也就是Android系统类内部,我们用的是filed injection方式。

这样,Hilt就可以找到AnalyticsAdapter类,并在需要(依赖于)它的地方,把它的实例对象注入进去。

4.4 Hilt Modules

细心的读者会发现,在AnalyticsAdapter类的构造函数中,同样需要依赖另外一个叫AnalyticsService的类。那么我们依葫芦画瓢,是不是只要在AnalyticsService类的构造函数前加上@inject注解就可以了呢?答案是不完全可以。

为什么那么说呢?

如果AnalyticsService是一个接口呢?接口又没有构造函数,所以也无法使用constructor injection的方式了(前面我们也提到过,android系统类和第三方库类也都无法使用constructor injection)。

这时候我们要引出Hilt的另外一个概念:Modules

Hilt module 也是一个类,但是它需要在类名前面加上注解(annotation)@Module;同时,还需要加上@InstallIn,顾名思义,我们拆开来看这个注解:Install In,后面肯定跟的是一个作用域范围(scope),意思就是告诉Hilt,我们新建的这个“Module”类,作用范围是什么。这个范围(scope)的分类,我们后续将会提到,在这暂且先不用深究。

4.4.1 使用@Binds注解进行接口实例注入

前面我们说过,接口是没有构造函数的(假如我们例子中的AnalyticsService是一个接口),所以也无法使用constructor injection的方式进行依赖注入,那么我们怎么样才能让Hilt知道我们需要这个接口Interface的实例作为依赖呢?

这时候需要使用注解(annotation)@Binds来告诉Hilt。

不过有一项准备工作我们还要先做,那就是先得有一个接口的实现类。这样Hilt才会知道使用哪个实现类去实现这个接口,相关示例代码如下:

interface AnalyticsService {
  fun analyticsMethods()
}

/*接口AnalyticsService 的实现类需要 Constructor-injected,
Hilt 需要使用到*/
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

AnalyticsService是一个接口,AnalyticsServiceImplAnalyticsService接口的实现类,同理AnalyticsServiceImpl的构造函数前也需要加上注解@Inject告诉Hilt在哪可以找到它。

最后就到我们的Hilt Module了,它前面有两个注解:@Module@InstallIn(ActivityComponent::class)。这两个注解总结起来就是告诉Hilt,这个Module的可见范围是所有的Activity类(因为我们需要为ExampleActivity类注入它需要的依赖(也就是AnalyticsService 的实例))。

因为返回的是AnalyticsService接口类型,所以我们这里使用抽象类和抽象函数bindAnalyticsService来实现这个Hilt Module。

抽象函数bindAnalyticsService前面是注解(annotation)@Binds,它的参数就是接口AnalyticsService的实现类AnalyticsServiceImpl,函数返回类型就是接口AnalyticsService。这样,Hilt就可以准确地把它的实例注入到依赖于它的地方了。

总结一下,就是对于接口实例的依赖注入,需要使用三个注解:@Module@InstallIn@Binds

4.4.2 使用注解@Provides实现第三方库依赖注入

当我们需要对第三方库进行依赖注入的时候,就不能使用注解(annotation)@Binds了,为了以示区分,Hilt提供了一个新的注解@Provides

其实Hilt的注解(annotation)取名还是很考究的,接口是需要“绑定”(@Binds)的,而第三方库是需要“提供”(@Provides)的。

假如AnalyticsService类实现了一个第三方库Retrofit,这时候,我们只需要在Hilt的Module中实现一个函数,函数名可以任意起,目的是要告诉Hilt,怎么来构建这个AnalyticsService类的实例对象,示例代码如下:

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

与前面的注解@Binds例子相比,在这个例子中主要是把@Binds改成了@Provides,函数provideAnalyticsService返回的是一个使用Retrofit构建的AnalyticsService类的实例。

这样,Hilt就可以在需要AnalyticsService类实例(依赖)的地方,把它的实例注入进去。

4.4.3 相同类型拥有不同的实现类如何依赖注入?

无论是接口类型,还是第三方库类型,都有可能会有这种应用场景:返回相同的接口/类,但是有多种实现/构建方式(存在不同的实例)。

比如前面的例子中,当AnalyticsService是一个接口时,它不仅有实现类AnalyticsServiceImpl,还有另外一个实现类AnalyticsServiceOhterImpl,这就会出现一个问题:Hilt怎么样才能知道在什么地方注入哪个具体的实例呢?

这个问题Hilt是不知道的,它需要程序员明确告诉它。但是程序员应该如何告诉Hilt呢?

Hilt提供了一个注解(annotation)@Qualifier。前面我们说过,Hilt的注解起名字还是很考究的,“Qualifier”字面上的意思就是“有资格的人”。程序员需要自己建立这么一个“有资格的”注解类。

使用这个Hilt提供的注解@Qualifier,可以生成相应的注解类annotation class。以此来告诉Hilt,这些接口的实现类是不同的,虽然返回的是相同的类型AnalyticsService,但是实现方式不一样,就是要根据程序员编写的注解类annotation class来进行区分。

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

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

接下来在Hilt的Module中把这个新写的注解使用到相应的函数中:

interface AnalyticsService {
  fun analyticsMethods()
}

class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

class AnalyticsServiceOtherImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @BindAnalyticsServiceImpl
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService

  @BindAnalyticsServiceOhterImpl
  @Binds
  abstract fun bindAnalyticsServiceOther(
    analyticsServiceOtherImpl: AnalyticsServiceOtherImpl
  ): AnalyticsService

}

然后在使用这个接口实例的地方,也需要加上新生成的相应的注解(@BindAnalyticsServiceImpl或者@BindAnalyticsServiceOhterImpl)。这样Hilt才能知道,什么地方需要哪个实例(依赖),然后提供出来。

同理,对于第三方库,使用的方法也是一样,不过只是把注解@Binds替换成@Provides,抽象类和抽象函数变成普通类普通函数而已。示例代码如下:

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

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


@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

然后在需要这些具体依赖的地方,同样声明新生成的注解:

@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

以上代码中的注解@AuthInterceptorOkHttpClient,就是告诉Hilt,这里需要的是函数provideAuthInterceptorOkHttpClient()返回的OkHttpClient实例。而不是函数provideOtherInterceptorOkHttpClient()返回的OkHttpClient实例。

他们都是一一对应的关系,所以如果使用了这种多绑定/提供(@Binds/@Provides)的方式,在需要其依赖的地方,除了Hilt提供的注解,例如@Inject以外,不要忘记了我们用@Qualifier自己建立的注解。否则程序是不会正常运行的。

4.4.4 Hilt内部预置的qualifers

qualifer,中文直译就是“有资格的人”。Hilt使用这个词,表示的就是可以“有资格”做某些事的注解。

就像我们在上一节中使用注解@Qualifier来新建不同实现实例类的注解时,表明的就是某些事情的“资格”。

同理,Hilt内部给我们预置了一些这样的qualifers,比如我们最熟悉的Context,几乎哪哪哪都需要。Hilt就帮我们内置了@ApplicationContext@ActivityContext这两种qualifers。

比如我们需要一个Activity Context ,只需要这么使用:

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

这里需要注意的一点是,如果AnalyticsAdapter类前面加上了注解Singleton,表示这个类的实例全局可见以后,是不可以使用@ActivityContext依赖注入的。要对应的使用@ApplicationContext;或者像上面的示例代码一样,AnalyticsAdapter前面不要加任何注解。

这里涉及到我们接下来要介绍的Hilt的另外一个知识点:Hilt的components(组件)及其Scope(作用域范围)。

4.5 Hilt的components(组件)及其Scope(作用域范围)

在文章前面部分,我们提到过,Hilt为每个Android系统类都生成了相应的component,而component,也有其相应的scope。

如下面的表格所示:

Android classGenerated componentScope
ApplicationApplicationComponent@Singleton
View ModelActivityRetainedComponent@ActivityRetainedScope
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
View annotated with @WithFragmentBindingsViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScoped

还记得在Hilt Module中一直使用的注解@InstallIn()么?其参数就是Hilt为Android class生成的components,其相应的作用于范围Scope就如同上表格中所示。

这里着重说说@Singleton这个作用域范围,类似于单例的意思。就是在全局中只有一份依赖(实例)。比如对于Retrofit和OkHttpClient的实例,以及ROOM数据库的实例,全局只需要一份就可以。

否则Hilt会为每次的依赖注入生成新的实例,这是Hilt的策略决定的,这样可以避免一些类的实例长期占用内存。

以下代码摘自Android Jetpack最佳实践的 sunflower project中对网络请求和数据库中使用Hilt作为依赖注入时,使用注解@Singleton标注的示例代码:

@InstallIn(ApplicationComponent::class)
@Module
class DatabaseModule {

    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return AppDatabase.getInstance(context)
    }

    @Provides
    fun providePlantDao(appDatabase: AppDatabase): PlantDao {
        return appDatabase.plantDao()
    }

    @Provides
    fun provideGardenPlantingDao(appDatabase: AppDatabase): GardenPlantingDao {
        return appDatabase.gardenPlantingDao()
    }
}


@InstallIn(ApplicationComponent::class)
@Module
class NetworkModule {

    @Singleton
    @Provides
    fun provideUnsplashService(): UnsplashService {
        return UnsplashService.create()
    }
}

Note: Hilt doesn’t generate a component for broadcast receivers because Hilt injects broadcast receivers directly from ApplicationComponent. (Hilt 没有给broadcast receivers生成component,是因为它直接使用ApplicationComponent给 broadcast receivers做依赖注入)

我们在介绍Hilt最开始的时候,说Hilt就帮我们干了两件事:提供 containers并管理其lifecycles。(个人人为,这里说的containers和components表达的是一个意思)

那么这些components的lifetimes是分别在哪建立和在哪销毁的呢?如下表所示:

Generated componentCreated atDestroyed at
ApplicationComponentApplication#onCreate()Application#onDestroy()
ActivityRetainedComponentActivity#onCreate()Activity#onDestroy()
ActivityComponentActivity#onCreate()Activity#onDestroy()
FragmentComponentFragment#onAttach()Fragment#onDestroy()
ViewComponentView#super()View destroyed
ViewWithFragmentComponentView#super()View destroyed
ServiceComponentService#onCreate()Service#onDestroy()

了解了Hilt components对应的Android系统类,以及对应的作用域范围及其相应的生命周期。我们终于把Hilt和Android开发的重要内容串联到了一起,脑海中开始有了一个直观的对照关系。

不过除此之外,我们还需要了解一点,关于Hilt的component Scope,就是其作用域范围,不单单局限在自身,而是有一个hierarchy(层级)的关系,如下图:
Hilt Components scope hierarchy
这张层级图是什么意思呢?

就像我们前面说的,关于Hilt的component 的Scope(作用域范围),不单单局限于自身。假如一个类使用注解@ActivityScoped,这个类可以不仅可以在ActivityComponent中进行依赖注入,还可以在其子component中,例如FragmentComponentViewComponent中也可以进行依赖注入。

4.6 自定义EntryPoint

前面我们说过,Hilt为Android系统类提供了相应的代码生成(components)功能。

目前为止,Hilt支持的Android系统类只有以下六种:除了Application使用@HiltAndroidApp注解标注外,其他5种都使用@AndroidEntryPoint注解。

  1. Application(使用@HiltAndroidApp注解标注)
  2. Activity
  3. Fragment
  4. View
  5. Service
  6. BroadcastReceiver

那么如果我们想要让Hilt帮我们给某一个我们需要的类生成component,应该怎么办呢?这时候需要使用Hilt提供的注解@EntryPoint

可能某些细心的读者已经发现,Hilt支持的Android系统类里面缺了一个同为四大组件之一的“content providers”,假如我们要让Hilt实现一个content provider的依赖注入,应该怎么做呢?

class ExampleContentProvider : ContentProvider() {

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

  ...
}

然后使用EntryPointAccessors去访问刚才建立的entry point:

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

注意这两者的Scope也要一致。例如前面建立entry point使用的是@InstallIn(ApplicationComponent::class),所以访问使用的是对应的ApplicationContext

个人认为,自定义entry point似乎用处不大,只要了解其存在即可,不用深究。

至于为什么Hilt唯独不支持四大组件之一的“content providers”,根据郭霖在其文章中的描述,可以作为参考:

主要原因就是ContentProvider的生命周期问题。如果你比较了解ContentProvider的话,应该知道它的生命周期是比较特殊的,它在Application的onCreate()方法之前就能得到执行,因此很多人会利用这个特性去进行提前初始化。

郭霖 《Jetpack新成员,一篇文章带你玩转Hilt和依赖注入

4.7 Hilt 对 Jetpack的支持(ViewModel,WorkManager)

Hilt对Jetpack的支持,目前主要体现在ViewModelWorkManager上。

4.7.1 ViewModel中的使用

在ViewModel的类中,使用的是constructor injection的方式:

class ExampleViewModel @ViewModelInject constructor(
  private val repository: ExampleRepository,
  @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
  ...
}

对比之前其他类的用法,就是把@Inject改成@ViewModelInject,并且给变量savedStateHandle标注@Assisted

然后在Activity中这么使用:

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  private val exampleViewModel: ExampleViewModel by viewModels()
  ...
}

其中,viewModels()函数来自KTX扩展类(同属于Jetpack)。

如果要在Jetpack navigation中使用ViewModel,则如下:

val viewModel: ExampleViewModel by navGraphViewModels(R.id.my_graph) {
  defaultViewModelProviderFactory
}

4.7.2 WorkManager中的使用

在WorkManager中使用Hilt,跟ViewModel也大同小异。

Hilt也给WorkManager提供了特有的注解@WorkerInject:

class ExampleWorker @WorkerInject constructor(
  @Assisted appContext: Context,
  @Assisted workerParams: WorkerParameters,
  workerDependency: WorkerDependency
) : Worker(appContext, workerParams) { ... }

然后在自定义的Application类中实现Configuration.Provider接口,注入HiltWorkFactory的实例:

@HiltAndroidApp
class ExampleApplication : Application(), Configuration.Provider {

  @Inject lateinit var workerFactory: HiltWorkerFactory

  override fun getWorkManagerConfiguration() =
      Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .build()
}

5 总结

依赖注入(Dependency Injection)几乎是所有面向对象语言不可或缺的技术。

现代软件的发展,可以让程序员更关注业务本身,将很多工作交给系统或者框架,以及很多优秀的第三方库来实现。

这是时代发展的必然趋势,但是其副作用也很明显:框架的存在,必定隐藏了很多功能实现的细节,就像网络上很多人对Hilt, 或者对Dagger的直观感受描述——像黑魔法一样。

这种知其然,不知其所以然的状态太多的话,很容易让人在碰到问题的时候不知道从哪开始下手解决。

所以本文试图从手动依赖注入开始,一步一步引出Hilt的基本实现原理,它能为我们做什么,以及我们应该如何使用它。最后再简要说明Hilt对同为Jetpack家族的ViewModel 和 WorkManager的支持。

本文仅从个人角度出发对依赖注入有些浅显的理解,并且没有严格校对,难免有错误遗漏之处,还请见谅。

参考文献:

  1. Dependency injection in Android
  2. Android Sunflower
  3. Jetpack新成员,一篇文章带你玩转Hilt和依赖注入
  4. Inversion of control
  5. Inversion of Control Containers and the Dependency Injection pattern
  6. Guide to app architecture

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值