Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 - 掘金 在多模块应用中使用 Hilt | Android 开发者 | Android Developers [[依赖注入内联函数]]
使用场景 为什么我们要使用依赖注入呢?解耦。
我们程序里有些对象是全局共享的,比如线程池,或者 Retrofit 对象,这种东西我们通常会把它放在 Application 对象里,或者做成单例的
还有些对象是局部共享的,比如某个 Activity 会把一些显示用的数据共享给它内部的一些 View 和 Fragment。这一类情况我们的做法通常是获取外部 Activity 对象然后强转,再去拿它内部的对象
除了共享的对象,不共享的也可以用依赖注入的方式来进行初始化,因为依赖注入的作用除了对共享对象提供一致性支持,也可以让我们在创建任何对象的时候省一些思考和力气
总之,如果一个组件可能会被被共享,或者不会被共享但可能会在多处使用,你都可以使用 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 注入的字段是不可以声明成 private1 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 之外的其他地方就无法使用了。 比如我们提供的 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 个内置组件分别是一一对应的,如下表所示。 如果想要在全程序范围内共用某个对象的实例,那么就使用 @Singleton。如果想要在某个 Activity,以及它内部包含的 Fragment 和 View 中共用某个对象的实例,那么就使用 @ActivityScoped。以此类推。
1 2 3 @Singleton class Driver @Inject constructor () { }
这就表示,Driver 在整个项目的全局范围内都会共享同一个实例,并且全局都可以对 Driver 类进行依赖注入。
而如果我们将注解改成 @ActivityScoped,那么就表示 Driver 在同一个 Activity 内部将会共享同一个实例,并且 Activity、Fragment、View 都可以对 Driver 类进行依赖注入。
@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 开发的话,那么可以无视这个关键字。