什么是依赖注入(dependency injection )
首先什么是依赖,这个很简单,我们编写一个Car类,Car类中需要并声明一个Engine类,这个Engine类就是Car类的依赖,当然也可以说Car类依赖于Engine类。
然后这个Engine类的创建有三种方式:
- 在Car类的内部,自己创建;
- 静态方法写入(如 Context getter 和 getSystemService());
- 由外部传进来,以参数或者构造方法中的形式提供。
其中第三种方式我们就称之为“依赖注入”。
也就是说“依赖注入”其实在我们日常的代码编写中很常见,比如我们的带参构造函数,建造者模式builder.xx(xx),工厂模式xxFactory.create(xx),甚至我们日常写的xxWidget中的setData() {mData = data},都是依赖注入。
为什么要试用依赖注入
设计模式六大原则中,有一个原则叫做依赖倒置原则:
抽象不应该依赖于具体类,具体类应当依赖于抽象。
也就是说我们在代码中要尽量引用层次高的抽象类、接口来进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
换言之,要针对接口编程,而不是针对实现编程。
这样做的好处是通过抽象来搭建框架,减少类与类之间的耦合性,以抽象搭建的系统要比以具体实现搭建的系统具有更高的扩展性,便于维护,同时也满足开闭原则的要求。
这里就不得不引入另一个名词叫控制反转(IOC Inversion Of Control),控制反转是根据依赖倒置原则衍生出来的一种编程思想。
用上面的例子来说:在Car中自己声明并创建Engine类,Engine类的控制权实际是在Car类的手中。但如果我们用依赖注入的方式将Engine类注入给Car的话,Engine类的实际控制权就不在Car的手中了,因为Car具体使用哪个Engine是由我们怎么传入Engine实例来决定的,如果用框架的话,Engine类的控制权就被框架所接管了。
而我们这里探讨的依赖注入,就是ioc思想下的一种具体的落地实现。具体的好处有:
重用组件: 因为我们在类外部构造依赖项;
组件解耦: 当我们需要修改某个组件的实现时,不需要在项目中进行大量变更;
易测试: 我们可以向依赖方注入依赖项的模拟实现,这使得依赖方的测试更加容易;
生命周期透明: 依赖方不感知依赖项创建 / 销毁的生命周期,这些可以交给依赖注入框架管理。
自动依赖注入库
从实现上,依赖注入框架可以归为两类:
- 基于反射的动态方案: Koin、Dagger、Guice;
- 基于编译时注解的静态方案(性能更高): Dagger2、Hilt。
Hilt是一个对Dagger库的扩展。Dagger 的名字取自有向无环图(DAG,Directed acyclic graph),起初是由Square公司开发的,当时采用的是运行时反射的方式,在2014年是过继给了Google公司接手,并完成了Dagger2.0的开发,改为了编译期生成代码的方式。
而Hilt也是Google基于Dagger开发的一个库,对Dagger做了Android场景化的处理,使之更符合我们Android开发者的使用习惯。(Dagger是没做场景区分的,可以用来开发后端、桌面端等,比如说Hilt则对Android studio做了适配,可以很方便的查看类之间的依赖关系)
而Hilt的用法也相较而言更简单:
添加依赖:
...
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
...
// 需要启用java8
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
}
// Allow references to generated code
kapt {
correctErrorTypes true
}
然后 application中:
@HiltAndroidApp
class ExampleApplication : Application() { ... }
Hilt 目前支持以下 Android 类:
- Application(通过使用 @HiltAndroidApp)
- ViewModel(通过使用 @HiltViewModel)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
使用时用 @AndroidEntryPoint 为某个 Android 类添加注解,还必须为依赖于该类的 Android 类添加注解。例如,如果您为某个 fragment 添加注解,则还必须为使用该 fragment 的所有 activity 添加注解。
然后用@Hilt注解需要注入的字段。
注意:由 Hilt 注入的字段不能为私有字段。尝试使用 Hilt 注入私有字段会导致编译错误。
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var user: User
...
}
data class User constructor(val id: String, val name: String) {
@Inject
constructor() : this("0", "哈哈")
}
这里的user对象就被注入进去了。这只是Hilt简便的写法,标准的注入写法应该是这样的:
@Module
@InstallIn(SingletonComponent::class)
object UserModule {
@Singleton
@Provides
fun providerUser() = User("2", "呵呵")
}
这个带 @Module 注解的类就称之为hilt模块,它会告知 Hilt 如何提供某些类型的实例。@Install 是告知 Hilt 每个模块将用在或安装在哪个 Android 类中。
比如 @InstallIn(ActivityComponent::class),就是把这个模块安装到Activity组件当中。那么在Activity、Fragment、View中是可以使用由这个模块提供的所有依赖注入实例。
而 @Singleton 是可选的,表示限定组件的作用域。@Singleton 表示整个Application作用域内,只创建这一个对象。
Hilt一共提供了如下几种组件,并会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。
如果不写则表示未限定作用域,即每次注入都会创建一个新的实例。
| Hilt组件 | 注入器面向的对象 | 作用域 |
|---|---|---|
| SingletonComponent | Application | @Singleton |
| ActivityRetainedComponent | Activity | @ActivityRetainedScoped |
| ViewModelComponent | ViewModel | @ViewModelScoped |
| ActivityComponent | Activity | @ActivityScoped |
| FragmentComponent | Fragment | @FragmentScoped |
| ViewComponent | View | @ViewScoped |
| ViewWithFragmentComponent | 带有 @WithFragmentBindings 注解的 View | @ViewScoped |
| ServiceComponent | Service | @ServiceScoped |
如果是接口也可以注入:
interface IUser {
fun getUser(): User
}
class UserImpl @Inject constructor() : IUser {
override fun getUser(): User {
return User("3", "张三")
}
}
@Module
@InstallIn(SingletonComponent::class)
interface UserModule {
@Binds
fun provideUser(impl: UserImpl): IUser
}
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var userimpl: IUser
...
}
而且Hilt作为Google官方推荐的依赖库,是有Android studio 支持的。具体来说,当依赖注入的地方,是可以直接在Android studio中通过鼠标点击,快速跳转到原始注入的地方的。
其原理就藏在Dagger的名字中:DAG (directed acyclic graph):有向无环图
dagger
因为程序里的依赖关系拼接起来就是一个或者多个有向无环图:
dag2_example.png
正是因为这些依赖关系的存在,Android studio可以帮我们找到并跳转到依赖相应的位置,也正应如此,hilt可以在编译器就对我们构建的依赖关系进行检查,如果有存在环状的依赖关系,则会在编译期就报错给我们,也就是只要编译通过了就说明依赖关系已经没问题了。
实际使用效果
下面以dataSource为例,看下使用后的效果:
class MessageDataSource @Inject constructor(
private val mMsgApi: MessageApi,
@Dispatcher(NetSchedulers.IO) private val io: Scheduler,
@Dispatcher(NetSchedulers.UI) private val ui: Scheduler
) : IMessageDataSource {
override fun queryMessage(number: Int): Observable<HitokotoMsg> {
return mMsgApi.getMessage("a")
.subscribeOn(io)
.observeOn(ui)
}
}
interface MessageApi {
@GET("/xx")
fun getMessage(@Query("c") c: String): Observable<HitokotoMsg>
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Dispatcher(val scheduler: NetSchedulers)
enum class NetSchedulers {
IO, UI
}
@Module
@InstallIn(SingletonComponent::class)
class NetModule {
@Dispatcher(scheduler = NetSchedulers.IO)
@Provides
fun providesIODispatcher(): Scheduler = Schedulers.io()
@Dispatcher(scheduler = NetSchedulers.UI)
@Provides
fun providesUIDispatcher(): Scheduler = AndroidSchedulers.mainThread()
@Provides
fun provideMessageApi(retrofit: Retrofit): MessageApi {
return retrofit.create(MessageApi::class.java)
}
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl("https://v1.hitokoto.cn/")
.client(okHttpClient)
.build()
}
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.build()
}
}
在整理代码逻辑的时候,我们只用关注类中用这个变量做了什么事,而不用关心变量具体是怎么来的,逻辑看着清爽了许多。想要看具体的依赖注入的来源,也可以利用Android studio的自动跳转功能一层层去查看,这些具体的依赖一般都放在XXModule中,看着也整洁了不少。
上面用@Qualifier注解标注的注解,这个注解就叫做限定符,是为了区分具体的依赖来源的。比如这里需要一个 Scheduler ,但这里我们提供了ui和io两个绑定,所以就需要自定义一个限定符注解加以区分。
Hilt 提供了一些预定义的限定符。例如来自应用或 activity 的 Context 类, Hilt 提供了
@ApplicationContext和@ActivityContext限定符。
其实也可以用@Name 属性来区分,不过由于存在硬编码,所以不太推荐。
@Provides
@Named("default")
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.build()
}
@Provides
@Named("loggingInterceptor")
fun provideOkHttpClient2(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(HttpLoggingInterceptor())
.build()
}
// 使用
@Inject
@Named("loggingInterceptor")
lateinit var okHttpClient: OkHttpClient
Hilt对象声明周期管理
上面的示例展示了Hilt两个方面的优势,代码的解耦和使用上的一些简化(用注解就能拿到对象,而不用到处new了)。Hilt还有个隐藏的优势就是对对象生命周期的管理。
场景一
比如说一个Activity中有两个Fragment,其中一个Fragment中有一个自定义View,在Activity的ViewModel中我们持有一个变量(又或者这两个Fragment之间有共享的变量)。如果我们想在自定义view中用上这个变量,要么得从Activity到Fragment,再到View,一层层塞进去;要么得从view往Fragment往Activity,写上两个接口一层层往上获取。
现在借助Hilt,我们有更简便的写法——直接注入。
场景二
想创造一个声明周期在Activity范围内的“单例对象”。
Hilt、Koin对比
Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 ,
hilt工作原理 ,
从 Dagger 到 Hilt,谷歌为何执着于让我们用依赖注入?












网友评论