环境
AndroidStudio(4.2.1) kotlin(kotlin_version = "1.3.71")
注意:kotlin_version = "1.5.0"时编译异常。
Jetpack新成员Hilt
依赖注入:简称DI(Dependency Injection)。一般用来解耦。Square在2012年推出Dagger框架,基于Java反射实现的(耗时,难使用)。Google开发fork了Dagger代码修改为Dagger2,基于注解实现,解决了反射的弊端。将一些简单的项目过度设计。Hilt借鉴了Dagger2使之简单、提供Android专属API。
引入Hilt
在项目跟目录build.gradle配置:
buildscript {
dependencies {
//
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
在app/build.gradle文件中:
plugins {
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
用Hilt,必须要自定义一个Application,否则Hilt将无法工作。 自定义MyApplication:
@HiltAndroidApp
class MyApplication :Application(){
...
}
然后将MyApplication在清单文件AndroidManifest.xml注册。 Hilt大幅简化了Dagger2的用法,不用通过@Component注解去编写桥接层逻辑;限定了只能从几个Android固定入口开始。 Hilt一共支持6个入口:
- Application
- Activity
- Fragment
- View
- Service
- BroadcastReceiver Application入口点使用@HiltAndroidApp注解声明。其他入口点,都使用@AndroidEntryPoint注解声明。 Activity入口点:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
}
向Activity中注入:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var truck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
truck.deliver()
}
}
lateinit是延迟初始化kotlin语法。@Inject注解来注入truck成员变量。注意Hilt注入的字段不可以声明称private。 Truck类:
class Truck @Inject constructor(){
fun deliver(){
println("run deliver")
}
}
通过@Inject注解,告诉Hilt,通过构造创建。
带参依赖注入
上面MainActivity不变。 Truck类:
class Truck @Inject constructor(val driver: Driver){
fun deliver(){
println("run deliver by:$driver")
}
}
class Driver @Inject constructor(){}
Truck通过构造增加了一个Driver参数;通过Driver类构造函数上声明一个@Inject注解。Truck的构造函数中所依赖的所有其他对象都支持依赖注入了,那么Truck才可以依赖注入。
接口的依赖注入
Engine接口:
interface Engine {
fun start()
fun shutdown()
}
具体实现类:依赖注入,GasEngine和ElectricEngine
class GasEngine @Inject constructor() : Engine {
override fun start() {
println("Gas start")
}
override fun shutdown() {
println("Gas shutdown")
}
}
class ElectricEngine @Inject constructor() : Engine {
override fun start() {
println("Electric start")
}
override fun shutdown() {
println("Electric shutdown")
}
}
新建抽象类实现桥接:EngineModule
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
}
@Module注解,提供依赖注入实例模块。提供Engine接口所需要的实例。抽象函数名自定义,抽象函数不需要具体实现,抽象函数的返回值必须是Engine,给Engine类型的接口提供实例。抽象函数接收了什么参数,就提供什么实例给它。抽象函数上加@Bind是Hilt才能识别到。当改变车辆引擎的时候,只需要修改EngineModule里抽象方法bindEngnie的参数就可以实现了。
给相同类型注入不同的实例
Qualifier注解,给相同类型的类或接口注入不同的实例。添加两个自定义注解:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine
@Retention用于声明注解的作用范围,选择AnnotationRetention.BINARY注解在编译之后会得到保存,但是无法通过反射去访问这个注解。 EngineModule类修改:
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@BindGasEngine
@Binds
abstract fun bindGasEngine(gasEngine: ElectricEngine): Engine
@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine
}
所有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("run deliver by:$driver")
gasEngine.shutdown()
electricEngine.shutdown()
}
}
解决了相同类型注入不同实例的问题。
第三方类的依赖注入
例如:okhttp 在项目的app/build.gradle中加入依赖:
implementation "com.squareup.okhttp3:okhttp:3.11.0"
定义类NetworkModule:
@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()
}
}
注意不是抽象类,因为这里需要实现第三方类初始化,而不是抽象函数。具体实现OkHttpClient创建。函数名自定义,返回值必须是OkHttpClient。provideOkHttpClient函数上加@Providers注解,Hilt才能识别。 在使用的时候:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var okHttpClient: OkHttpClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println("okhttp-user:$okHttpClient")
}
}
解决了第三方依赖的问题,但是用Okhttp人越来越少了,更多的用Retrofit作为网络请求库,而Retrofit实际对Okhttp的封装。我们希望NetworkModule中提供Retrofit类型:
@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 providerRetrofit(okHttpClient: OkHttpClient):Retrofit{
return Retrofit.Builder()
.baseUrl("http://baidu.com")
.client(okHttpClient)
.build();
}
}
定义一个provderRetrofit函数,去创建Retrofit实例返回。providerRetrofit接收了一个OkHttpClient参数,这个是不需要传递的。因为Hilt会自己寻找到provideOkHttpClient的OkHttpClient实现。 在用的地方:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var retrofit: Retrofit
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println("retrofit-user:$retrofit")
}
}
Hilt内置组件和组件作用域
@InstallIn(ActivityComponent::class),把这个模块安装到Activity组件中。那么Activity可以使用由这个模块提供的所有依赖注入实例。Activity包含的Fragment和View也可以使用,但是除了Activity、Fragment、View之外的其他地方无法使用。比如:Service使用@Inject来对Retrofit类型字段进行依赖注入,会报错。
Hilt内置7种组件类型:
- ApplicationComponent: Application
- ActivityRetainedComponent: ViewModel
- ActivityComponent: Activity
- FragmentComponent: Fragment
- ViewComponent: View
- ViewWithFragmentComponent: View annotated with @WithFragmentBindings
- ServiceComponent: Service 每个组件的作用范围不同。ApplicationComponent提供的依赖注入实际可以在全项目中使用,如果希望NetworkModule提供的Retrofit实例能在Service中进行依赖,可以:
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
}
Hilt作用域,每次依赖注入行为都创建不同实例。在一些情况下不合理的。有些情况全局只用一个实例,每次创建是不合理的。可以通过@Singleton来处理。
@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 providerRetrofit(okHttpClient: OkHttpClient):Retrofit{
return Retrofit.Builder()
.baseUrl("http://baidu.com")
.client(okHttpClient)
.build();
}
}
可以保证OkhttpClient和Retrofit全局只会存在一份实例。类--组件--作用域
如果想在全程序内共用某个对象实例,使用@Singleton。想在某个Activity,以及它内部包含Fragment和View中共用某个对象实例,用@ActivityScoped。不必须非得在某个Module中使用作用域注解,也可以直接将它声明到任何类的上方,比如:
@Singleton
class Driver @Inject constructor() {}
Driver在整个项目全局范围内共享一个实例,并且全局都可以对Driver进行依赖注入。如果改为@ActivityScoped,那Driver在同一个Activity内部将会共享一个实例,并且Activity、Fragment、View都可以对Driver类进行依赖注入。包含关系:
预置Qualifier
如果类Driver需要Context参数:
@Singleton
class Driver @Inject constructor(val context:Context) {}
会报错,因为Context这个参数不知道谁提供的。模仿Dirver被Truck引用的方法,在构造函数上加@Inject注解,无法解决,因为没有Context类的编写权限。用NetworkModule中@Module的方式,以第三方类形式给Context提供依赖注入:
@Module
@InstallIn(ApplicationComponent::class)
class ContextModule {
@Provides
fun provideContext(): Context {
???
}
}
但是具体怎样实现呢,因为不能直接new一个Context实例。Context是系统组件,实例由Android系统去创建的,所以前面的一些方法无法实现。 需要通过预置@Qualifier,提供Context类型的依赖注入实例。加上@ApplicationContext
@Singleton
class Driver @Inject constructor(@ApplicationContext val context:Context) {}
Hilt会自动提供一个Application类型的Context给Truck类,然后Truck类可以用Context写业务逻辑。如果需要的不是Application类型的Context,而是Activity类型的Context,Hilt另外一种Qualifier,@ActivityContext
@ActivityScoped
class Driver @Inject constructor(@ActivityContext val context:Context) {}
上面用了@ActivityScoped,因为@Singleton编译时报错(因为是全局的所以报错)。也可以使用@FragmentScoped、@ViewScoped或者直接删除掉都可以,这样就不会报错。 Qualifier对于Application和Activity类型,Hilt预置好了注入功能。如果依赖于Application或者Activity,不需要提供依赖注入的实例,Hilt自动识别:
class Driver @Inject constructor(val application:Application) {}
class Driver @Inject constructor(val activity:Activity) {}
编译可以直接通过,无需添加任何注解声明。 必须是Application和Activity类型,它们的子类型,编译都无法通过。 如果参数是MyApplication:
@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {
@Provides
fun provierMyApplication(application: Application): MyApplication {
return application as MyApplication
}
}
class Driver @Inject constructor(val application: MyApplication) {}
provierMyApplication函数接收到一个Application参数,这个参数是Hilt自动识别的,然后我们把它向下转型成MyApplication即可。在Truck类中就可以声明依赖了。
ViewModel依赖注入
在MVVM架构中,由Hilt去管理仓库层实例创建很合适。
第一中方式:
仓库层Repository:
class Repository @Inject constructor(){}
Repository要依赖注入到ViewModel中,所以给构造函数加@Inject注解。 MyViewModel继承自ViewModel,用于ViewModel层:
@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository):ViewModel(){}
- MyViewModel声明为@ActivityRetainedScoped注解,参照上面作用域表,这个注解专门为ViewModel提供,生命周期和ViewModel一致。
- MyViewModel的构造函数中要声明为@Inject注解,因为Activity中要用到依赖注入的方式获得MyViewModel实例。
- MyViewModel构造函数要加上Repository参数,表示MyViewModel是依赖于Repository的。 在MainActivity中通过依赖注入方式获得MyViewModel的实例:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println("viewModel-user:$viewModel")
}
}
但是有缺点:本来只想对Repository依赖注入,但是MyViewModel也依赖注入了。
第二种:
我们不想ViewModel也跟着依赖注入。Hilt专门提供了独立的依赖方式,在app/build.gradle中添加依赖:
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
修改MyViewModel代码:
class MyViewModel @ViewModelInject constructor(val repository: Repository):ViewModel(){}
@ActivityRetainedScoped注解移除,不需要它。@Inject注解改为@ViewModelInject注解,专门为ViewModel使用的。在MainActivity中:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel:MyViewModel by lazy {
ViewModelProvider(this).get(MyViewModel::class.java)
}
println("viewModel-user:$viewModel")
}
}
这种写法,虽然我们在MainActivity中没有使用依赖注入功能,但是@AndroidEntryPoint注解不能少。不然编译时期Hilt检测不出语法异常,到运行时期,Hilt找不到入口点无法执行依赖注入了。
不支持的入口点
Hilt支持的组件中ContentProvider(四大组件之一)。ContentProvider生命周期比较特殊,在Application的onCreate方法前执行,一些第三方库通过这个方法进行初始化操作(Jetpack成员 App Startup)。而Hilt的原理是从Application的onCreate方法开始的,这个方法执行前,Hilt的所有功能都还无法正常工作。Hilt才没有将ContentProvider纳入到支持的入口点中。 可以通过其他方法依赖注入功能。
class MyContentProvider :ContentProvider(){
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MyEntryPoint{
fun getRetrofit():Retrofit
}
override fun onCreate(): Boolean {
println("provider==onCreate")
context?.let {
val appContext = it.applicationContext
val entryPoint = EntryPointAccessors.fromApplication(appContext,MyEntryPoint::class.java)
val retrofit = entryPoint.getRetrofit()
println("ContentProvider==retrofit:$retrofit")
}
return false
}
}
定义MyEntryPoint接口,用@EntryPoint声明自定义入口点,用@InstallIn来声明作用范围。MyEntryPoint中定义了getRetrofit函数,返回类型是Retrofit。Retrofit上面已经写过注入,在NetworkModule中完成初始化。借助EntryPointAccessors类,调用fromApplication函数来获得自定义入口点实例。