白话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项目。
上图中的箭头是单方向的,意思就是箭头的一端依赖于另外一端。
比如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架构应该是下面这样的:
上图的单箭头跟前面 Android MVVM架构图里的单箭头意义是一样的——前者依赖于后者。
即LoginActivity
依赖于LoginViewModel
, LoginViewModel
依赖于UserRepository
, UserRepository
依赖于UserLocalDataSource
和UserRemoteDataSource
。最后,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干了哪两件事吗?
- 提供了“containers”(容器)用来装各种依赖;
- 并自动管理这些“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干了哪两件事?
- 提供了“containers”(容器)用来装各种依赖;
- 并自动管理这些“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()
}
}
最后,我们终于通过手动依赖注入的方式,实现了我们要达到的代码解耦的目的。
现在我们来回顾一下,我们都做了哪些工作:
- 首先新建了一个
AppContainer
类,把LoginViewModel
需要的各种依赖一股脑都放进去; - 为了在应用的其他地方(除了
LoginActivity
以外)使用LoginViewModel
的实例,我们用工厂类的设计模式在AppContainer
类容器中实现了一个loginViewModelFactory
对象; - 最后为了实现不同用户的登录和登出,我们又新建了一个
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系统类有以下六种:
Application
(使用@HiltAndroidApp
注解标注)Activity
Fragment
View
Service
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
是一个接口,AnalyticsServiceImpl
是AnalyticsService
接口的实现类,同理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 class | Generated component | Scope |
---|---|---|
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 |
还记得在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 component | Created at | Destroyed at |
---|---|---|
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() |
了解了Hilt components对应的Android系统类,以及对应的作用域范围及其相应的生命周期。我们终于把Hilt和Android开发的重要内容串联到了一起,脑海中开始有了一个直观的对照关系。
不过除此之外,我们还需要了解一点,关于Hilt的component Scope,就是其作用域范围,不单单局限在自身,而是有一个hierarchy(层级)的关系,如下图:
这张层级图是什么意思呢?
就像我们前面说的,关于Hilt的component 的Scope(作用域范围),不单单局限于自身。假如一个类使用注解@ActivityScoped
,这个类可以不仅可以在ActivityComponent
中进行依赖注入,还可以在其子component中,例如FragmentComponent
和ViewComponent
中也可以进行依赖注入。
4.6 自定义EntryPoint
前面我们说过,Hilt为Android系统类提供了相应的代码生成(components)功能。
目前为止,Hilt支持的Android系统类只有以下六种:除了Application
使用@HiltAndroidApp
注解标注外,其他5种都使用@AndroidEntryPoint
注解。
Application
(使用@HiltAndroidApp
注解标注)Activity
Fragment
View
Service
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()方法之前就能得到执行,因此很多人会利用这个特性去进行提前初始化。
4.7 Hilt 对 Jetpack的支持(ViewModel,WorkManager)
Hilt对Jetpack的支持,目前主要体现在ViewModel和WorkManager上。
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的支持。
本文仅从个人角度出发对依赖注入有些浅显的理解,并且没有严格校对,难免有错误遗漏之处,还请见谅。
参考文献:
- Dependency injection in Android
- Android Sunflower
- Jetpack新成员,一篇文章带你玩转Hilt和依赖注入
- Inversion of control
- Inversion of Control Containers and the Dependency Injection pattern
- Guide to app architecture