Hilt

Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 - 掘金
在多模块应用中使用 Hilt  |  Android 开发者  |  Android Developers
[[依赖注入内联函数]]

使用场景

为什么我们要使用依赖注入呢?解耦。

  1. 我们程序里有些对象是全局共享的,比如线程池,或者 Retrofit 对象,这种东西我们通常会把它放在 Application 对象里,或者做成单例的
  2. 还有些对象是局部共享的,比如某个 Activity 会把一些显示用的数据共享给它内部的一些 View 和 Fragment。这一类情况我们的做法通常是获取外部 Activity 对象然后强转,再去拿它内部的对象
  3. 除了共享的对象,不共享的也可以用依赖注入的方式来进行初始化,因为依赖注入的作用除了对共享对象提供一致性支持,也可以让我们在创建任何对象的时候省一些思考和力气

总之,如果一个组件可能会被被共享,或者不会被共享但可能会在多处使用,你都可以使用 Hilt 来把它配置成依赖注入的加载方式。

使用

相比于 Dagger2,Hilt 最明显的特征就是:1. 简单。2. 提供了 Android 专属的 API。

Hilt 的简单用法

Hilt 当中,你必须要自定义一个 Application 才行,否则 Hilt 将无法正常工作。
这里我们自定义一个 MyApplication 类,代码如下所示:

1
2
3
@HiltAndroidApp
class MyApplication : Application() {
}

Hilt 一共支持 6 个入口点,分别是:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver
    有 Application 这个入口点是使用 @HiltAndroidApp 注解来声明的,这个我们刚才已经看过了。其他的所有入口点,都是用 @AndroidEntryPoint 注解来声明的。
    Hilt 注入的字段是不可以声明成 private
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Truck @Inject constructor() {

    fun deliver() {
    println("Truck is delivering cargo.")
    }

    }

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var truck: Truck

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    truck.deliver()
    }

    }

带参数的依赖注入

1
2
3
4
5
6
7
8
9
class Truck @Inject constructor(val driver: Driver) {

fun deliver() {
println("Truck is delivering cargo. Driven by $driver")
}

}

class Driver @Inject constructor() { }

接口的依赖注入

1
2
3
4
5
6
7
8
class GasEngine @Inject constructor() : Engine {
...
}

class ElectricEngine @Inject constructor() : Engine {
...
}

接下来我们需要新建一个抽象类,类名叫什么都可以,但是最好要和业务逻辑有相关性,因此我建议起名 EngineModule. kt,最后,在抽象函数上方加上 @Bind 注解,这样 Hilt 才能识别它。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine

}
class Truck @Inject constructor(val driver: Driver) {

@Inject
lateinit var engine: Engine

fun deliver() {
engine.start()
println("Truck is delivering cargo. Driven by $driver")
engine.shutdown()
}

}

给相同类型注入不同的实例

Qualifier 注解。给相同类型的类或接口注入不同的实例。

1
2
3
4
5
6
7
8
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine

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

至于另外一个 @Retention,是用于声明注解的作用范围,选择 AnnotationRetention.BINARY 表示该注解在编译之后会得到保留,但是无法通过反射去访问这个注解。这应该是最合理的一个注解作用范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

@BindGasEngine
@Binds
abstract fun bindGasEngine(gasEngine: GasEngine): Engine

@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine

}

class Truck @Inject constructor(val driver: Driver) {

@BindGasEngine
@Inject
lateinit var gasEngine: Engine

@BindElectricEngine
@Inject
lateinit var electricEngine: Engine

fun deliver() {
gasEngine.start()
electricEngine.start()
println("Truck is delivering cargo. Driven by $driver")
gasEngine.shutdown()
electricEngine.shutdown()
}

}


第三方类的依赖注入

借助 @Module 注解,它的解决方案有点类似于刚才给接口类型提供依赖注入,但是并不完全一样。
记得要在 provideOkHttpClient() 函数的上方加上 @Provides 注解,这样 Hilt 才能识别它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {

@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}

@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com/")
.client(okHttpClient)
.build()
}

}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var okHttpClient: OkHttpClient

@Inject
lateinit var retrofit: Retrofit
...

}

Hilt 内置组件和组件作用域

InstallIn,就是安装到的意思。那么 @InstallIn(ActivityComponent::class),就是把这个模块安装到 Activity 组件当中。
Activity 中包含的 Fragment 和 View 也可以使用,但是除了 Activity、Fragment、View 之外的其他地方就无法使用了。
image.png|600
比如我们提供的 Retrofit 和 OkHttpClient 的实例,理论上它们全局只需要一份就可以了,每次都创建不同的实例明显是一种不必要的浪费。
而更改这种默认行为其实也很简单,借助 @Singleton 注解即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {

@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}

@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com")
.client(okHttpClient)
.build()
}

}

Hilt 一共提供了 7 种组件作用域注解,和刚才的 7 个内置组件分别是一一对应的,如下表所示。
image.png|600
如果想要在全程序范围内共用某个对象的实例,那么就使用 @Singleton。如果想要在某个 Activity,以及它内部包含的 Fragment 和 View 中共用某个对象的实例,那么就使用 @ActivityScoped。以此类推。

1
2
3
@Singleton
class Driver @Inject constructor() {
}

这就表示,Driver 在整个项目的全局范围内都会共享同一个实例,并且全局都可以对 Driver 类进行依赖注入。

而如果我们将注解改成 @ActivityScoped,那么就表示 Driver 在同一个 Activity 内部将会共享同一个实例,并且 Activity、Fragment、View 都可以对 Driver 类进行依赖注入。

image.png|600
@Singleton 注解的箭头可以指向所有地方。而 @ServiceScoped 注解的箭头无处可指,所以只能限定在 Service 自身当中使用。@ActivityScoped 注解的箭头可以指向 Fragment、View 当中。

预置 Qualifier

这种写法 Hilt 会自动提供一个 Application 类型的 Context 给到 Truck 类当中,然后 Truck 类就可以使用这个 Context 去编写具体的业务逻辑了。

但是如果你说,我需要的并不是 Application 类型的 Context,而是 Activity 类型的 Context。也没有问题,Hilt 还预置了另外一种 Qualifier,我们使用 @ActivityContext 即可:

1
2
3
4
@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}

关于预置 Qualifier 其实还有一个隐藏的小技巧,就是对于 Application 和 Activity 这两个类型,Hilt 也是给它们预置好了注入功能。也就是说,如果你的某个类依赖于 Application 或者 Activity,不需要想办法为这两个类提供依赖注入的实例,Hilt 自动就能识别它们。如下所示:

1
2
3
4
5
6
7
class Driver @Inject constructor(val application: Application) {
}

class Driver @Inject constructor(val activity: Activity) {
}


小窍门,因为 Application 全局只会存在一份实例,因此 Hilt 注入的 Application 实例其实就是你自定义的 MyApplication 实例,所以想办法做一下向下类型转换就可以了。

1
2
3
4
5
6
7
8
9
10
11
@Module
@InstallIn (ApplicationComponent::class)
class ApplicationModule {

@Provides
fun provideMyApplication(application: Application): MyApplication {
return application as MyApplication
}

}

ViewModel 的依赖注入

对于 ViewModel 这种常用 Jetpack 组件,Hilt 专门为其提供了一种独立的依赖注入方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dependencies {
...
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}

class MyViewModel @ViewModelInject constructor(val repository: Repository) : ViewModel() {
...
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
...

}

不支持的入口点怎么办?

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

1
2
3
4
5
6
7
8
9
10
class MyContentProvider : ContentProvider() {

@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MyEntryPoint {
fun getRetrofit(): Retrofit
}
...

}

总结

最新版有两个不一样的地方,单例不再是 applicationComponent,改成 SingletonComponent 了,第二是 ViewModel 注入可以改成 HiltViewModel

  • @Module 注解,表示这一个用于提供依赖注入实例的模块,接口或第三方用
  • @Binds:接口,需要在方法参数里面明确指明接口的实现类。还要再定义个注解 Qualifier
  • @Provides:不需要在方法参数里面明确指明接口的实现类,由第三方框架实现,通常用于和第三方框架进行绑定 (Retrofit、Room 等等)

首先 lateinit 是 Kotlin 中的关键字,和 Hilt 无关。这个关键字用于对变量延迟初始化,因为 Kotlin 默认在声明一个变量时就要对其进行初始化,而这里我们并不想手动初始化,所以要加上 lateinit。如果你是用 Java 开发的话,那么可以无视这个关键字。


Hilt
http://peiniwan.github.io/2024/04/49492bb35a1e.html
作者
六月的雨
发布于
2024年4月6日
许可协议