Jetpack学习-1-Hilt

966 阅读3分钟

环境

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个入口:

  1. Application
  2. Activity
  3. Fragment
  4. View
  5. Service
  6. 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种组件类型:

  1. ApplicationComponent: Application
  2. ActivityRetainedComponent: ViewModel
  3. ActivityComponent: Activity
  4. FragmentComponent: Fragment
  5. ViewComponent: View
  6. ViewWithFragmentComponent: View annotated with @WithFragmentBindings
  7. 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全局只会存在一份实例。类--组件--作用域

image.png 如果想在全程序内共用某个对象实例,使用@Singleton。想在某个Activity,以及它内部包含Fragment和View中共用某个对象实例,用@ActivityScoped。不必须非得在某个Module中使用作用域注解,也可以直接将它声明到任何类的上方,比如:

@Singleton
class Driver @Inject constructor() {}

Driver在整个项目全局范围内共享一个实例,并且全局都可以对Driver进行依赖注入。如果改为@ActivityScoped,那Driver在同一个Activity内部将会共享一个实例,并且Activity、Fragment、View都可以对Driver类进行依赖注入。包含关系:

image.png

预置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(){}
  1. MyViewModel声明为@ActivityRetainedScoped注解,参照上面作用域表,这个注解专门为ViewModel提供,生命周期和ViewModel一致。
  2. MyViewModel的构造函数中要声明为@Inject注解,因为Activity中要用到依赖注入的方式获得MyViewModel实例。
  3. 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函数来获得自定义入口点实例。